Location: PHPKode > projects > Meeting Request Scheduling and Booking System > mrsbs/php/lib/functions.php
<?PHP

   // Function definitions
   //
   // Version: $Revision: 1.24 $
   // Date: $Date: 2008/06/22 16:50:41 $
   //
   // Copyright (c) 2006 - 2008 Benjamin Oshrin
   // License restrictions apply, see LICENSE for details.

function add_contact($givenname, $sn, $mail)
{
  // Add a new contact, with $givenname, $sn, and $mail.

  // Returns: The new contact ID if fully successful, false otherwise.

  global $dbc;

  $contactid = $dbc->GenID('mrsbs_contactid_seq', 1001);

  $sql = $dbc->Prepare('INSERT INTO mrsbs_contacts
                        (contactid, mail, givenname, sn)
                        VALUES (' . $dbc->Param('a') . ',' .
		        $dbc->Param('b') . ',' .
		        $dbc->Param('c') . ',' .
		        $dbc->Param('d') . ')');
  
  if($dbc->Execute($sql, array($contactid,
			       maybe_obscure_email($mail),
			       $givenname, $sn)))
    return($contactid);
  else
    return(false);
}

function add_contact_and_invite($givenname, $sn, $mail, $mtgid, $status)
{
  // Add a new contact, with $givenname, $sn, and $mail, and invite the
  // new contact to $mtgid with $status.

  // Returns: The new contact ID if fully successful, false otherwise.

  global $dbc;
  $ret = false;
  
  $dbc->StartTrans();
  
  $contactid = add_contact($givenname, $sn, $mail);

  if($contactid)
  {
    if(add_meeting_invitee($mtgid, $contactid, $status) == 1)
      $ret = true;
    else
      $dbc->FailTrans();
  }
  else
    $dbc->FailTrans();
  
  $dbc->CompleteTrans($sql);

  return($ret);
}

function add_delegate($delegator, $delegate, $pcreate, $preply)
{
  // Add a new $delegate for $delegator, with permission $pcreate and/or
  // $preply.

  // Returns: true if fully successful, false otherwise.

  global $dbc;
  $ret = false;

  $sql = $dbc->Prepare('INSERT INTO mrsbs_delegates
                        (contactid, delegateid, pcreate, preply)
                        VALUES (' . $dbc->Param('a') . ',' .
		        $dbc->Param('b') . ',' . $dbc->Param('c') . ',' .
		        $dbc->Param('d') . ')');
  
  return($dbc->Execute($sql, array($delegator,
				   $delegate,
				   ($pcreate == "Y" ? "'Y'" : "''"),
				   ($preply == "Y" ? "'Y'" : "''"))));
}

function add_location($desc, $capacity, $contactid, $system, $acl)
{
  // Add a new location, with description $desc, $capacity, owner $contactid,
  // $system type, and $acl (of the form used by update_acls).

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $dbc->StartTrans();
  
  $locationid = $dbc->GenID('mrsbs_locationid_seq', 1001);

  $sql = $dbc->Prepare('INSERT INTO mrsbs_locations
                        (locationid, description, capacity, contactid, system)
                        VALUES (' . $dbc->Param('a') . ',' .
		        $dbc->Param('b') . ',' . $dbc->Param('c') . ',' .
		        $dbc->Param('d') . ',' . $dbc->Param('e') . ')');
  
  if($dbc->Execute($sql,
		   array($locationid, $desc, $capacity, $contactid, $system)))
    update_acls("L", $locationid, $acl);

  return($dbc->CompleteTrans());
}

function add_meeting_invitee($mtgid, $contactid, $status)
{
  // Add $contactid and $status to $mtgid.

  // Returns: 1 if fully successful, -1 if $contactid is already
  // invited, or 0 otherwise.

  global $dbc;

  $sql = $dbc->Prepare('SELECT inviteid FROM mrsbs_invitees wHERE
                        mtgid=' . $dbc->Param('a') .
		        ' AND contactid=' . $dbc->Param('b'));

  if($dbc->GetOne($sql, array($mtgid, $contactid)))
    return(-1);
  
  $inviteid = random_identifier();

  $sql = $dbc->Prepare('INSERT INTO mrsbs_invitees
                        (inviteid, mtgid, contactid, status, reply)
                        VALUES (' . $dbc->Param('a') . ',' .
		        $dbc->Param('b') . ',' . $dbc->Param('c') . ',' .
		        $dbc->Param('d') . ',' . $dbc->Param('e') . ')');
  
  $records = $dbc->Execute($sql,
			   array($inviteid, $mtgid, $contactid, $status, "N"));

  // Update recent invitee list, but don't adjust return value from it

  update_recent_invitees($_SESSION['contactid'], $contactid);
  
  return($records ? 1 : 0);
}

function add_meeting_window($mtgid, $begintime, $endtime)
{
  // Add a potential window to $mtgid, starting at epoch time $begintime
  // and ending at (non-inclusively) $endtime.

  // Returns: true if fully successful, false otherwise.

  global $dbc;
  
  $windowid = $dbc->GenID('mrsbs_windowid_seq', 1001);
  
  // We use count(*) rather than max(pref) because count always returns
  // a number while max returns NULL if there are no rows.

  $sql = 'INSERT INTO mrsbs_windows
          (windowid, mtgid, pref, begin, end)
          SELECT ' . $windowid . ',' . sstr($mtgid) . ', COUNT(*)+1, ' .
          $dbc->DBTimeStamp($begintime) . ',' . 
          $dbc->DBTimeStamp($endtime) . '
          FROM mrsbs_windows WHERE mtgid=' . sstr($mtgid);
    
  $records = $dbc->Execute($sql);

  return($records ? 1 : 0);
}

function authorize($component, $op, $p1='', $p2='')
{
  // Determine if the operation $op, corresponding to an operation
  // in an $component-op-handler.php switch statement, is authorized for the
  // current user and $p1/$p2 (component and operation specific parameters).

  // Returns: true if authorized, false otherwise.

  // An admin should always be authorized

  global $config;
  
  if(isset($_SESSION['user']) && isset($config['admin']))
  {
    // Check for explicitly named admin
    
    foreach($config['admin'] as $a)
    {
      if($a == $_SESSION['user'])
	return(true);
    }
  }

  if(isset($_SESSION['groups']) && isset($config['admingroups']))
  {
    // Check for admin by group membership
    
    foreach($config['admingroups'] as $a)
    {
      foreach($_SESSION['groups'] as $g)
      {
	if($a == $g)
	  return(true);
      }
    }
  }
  
  // Otherwise look at what the requested operation was.  Note if an
  // item isn't listed below, it is an admin-only operation.
  
  switch($component)
  {
  case "admin":
    switch($op)
    {
    case "admin":
      // Only return true if administrator (which was already checked)
      break;
    case "contact":
      // Only the contactid may manipulate her or his entry
      if(isset($_SESSION['contactid'])
	 && (($_SESSION['contactid'] == $p1)
	     || ($p1 == "self")))
	return(true);
      break;
    case "contacts":
    case "contactnew":
      // Any authenticated user (but not any contactid) may see contacts
      // and create new ones
      if(isset($_SESSION['user']) && $_SESSION['user'] != "")
	return(true);
      break;
    case "delcontact":
      // Only the admin can delete a contact
      break;
    case "location":
      // Only the owner of a location may manipulate it
      $ownerid = get_location_owner($p1);
      if(isset($_SESSION['contactid']) && $ownerid
	 && ($_SESSION['contactid'] == $ownerid))
	return(true);
      break;
    case "locationowner":
      // Only the admin can change the owner of a location
      break;
    case "locations":
    case "locnew":
      // Any authenticated user (but not any contactid) may see locations
      // and create new ones
      if(isset($_SESSION['user']) && $_SESSION['user'] != "")
	return(true);
      break;
    }
    break; // admin
  case "schedule":
    $ownerid = get_meeting_owner($p1);
    $hostid = get_meeting_host($p1);
    switch($op)
    {
    case "begin":
    case "what":
      // Any authenticated user (but not any contactid) may create a meeting
      // "what" is like "begin" if mtgid is -1
      if($op == "begin" || ($op == "what" && $p1 == -1))
      {
	if(isset($_SESSION['user']) && $_SESSION['user'] != "")
	  return(true);
	break;
      }
      // else fall through -- do NOT break
    case "host":
      // An owner can set the host of a meeting to themself or anyone
      // the can delegate for.  $p2 is the proposed host contactid.
      if(isset($_SESSION['contactid']) && $ownerid
	 && ($_SESSION['contactid'] == $ownerid)
	 && (($ownerid == $p2)
	     || (authorize_delegate("pcreate", $ownerid, $p2))))
	return(true);
      break;
    case "meetings":
      // Any authed person can see or search their own meetings
      if($p1 == "own" || $p1 == "search")
	return(true);
      break;
    case "owner":
      // Only an admin can change the owner of a meeting
      break;
    case "calculate":
    case "cancel":
    case "clone";
    case "confirmloc":
    case "history":
    case "location":
    case "notify":
    case "null":
    case "recalculate":
    case "review":
    case "send":
    case "sendnotify":
    case "uncancel":
    case "viewreply":
    case "what":
    case "when":
    case "when2":
    case "whennew":
    case "where":
    case "who":
    case "who2":
    case "whonew":
    case "whorecent":
      // Only the owner or host of a meeting may manipulate it
      if(isset($_SESSION['contactid']) && $ownerid
	 && (($_SESSION['contactid'] == $ownerid)
	     || ($_SESSION['contactid'] == $hostid)))
	return(true);
      break;
    }
    break; // schedule
  }
  
  return(false);
}

function authorize_delegate($op, $who, $for)
{
  // Determine if $who can perform $op on behalf of $for.

  // Returns: true if permission is granted, false otherwise.

  global $dbc;

  $sql = $dbc->Prepare('SELECT ' . $op . ' FROM mrsbs_delegates
                        WHERE delegateid=' . $dbc->Param('a') . '
                        AND contactid=' . $dbc->Param('b'));
  
  if($dbc->GetOne($sql, array($who, $for)) == 'Y')
    return(true);
  else
    return(false);
}

function build_uri($doc = "")
{
  // Build a URI usable to retrieve $doc.  If $doc is empty, obtain
  // the URI used to request the current page.

  // Returns: The URI.

  global $config;
  
  if(isset($_SERVER['SERVER_NAME']))
  {
    // Dynamically build the string based on the server settings

    $ret = "http";

    if(isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on')
      $ret .= "s";

    $ret .= "://" . $_SERVER['SERVER_NAME'];

    if($doc == "")
      $ret .= $_SERVER['REQUEST_URI'];
    else
    {
      // We need to find out document root, which may not be /.
      // Except for /index.php, all pages should be two levels deep.

      $docroot = preg_split('/\//', $_SERVER['PHP_SELF']);

      if($docroot[count($docroot)-1] == "index.php")
      {
	// Toss the last component
	array_pop($docroot);
      }
      else
      {
	// Toss the last two components
	array_pop($docroot);
	array_pop($docroot);
      }
      
      $ret .= implode("/", $docroot) . $doc;
    }
  }
  else
  {
    // We are probably running via cli.  Use config file settings.

    $ret = $config['uriprefix'] . $doc;
  }

  return($ret);
}

function build_uri_args($trim, $args, $vals, $doc = "")
{
  // Build an encoded URI usable to retrieve $doc, with $args appended
  // suitably.  If $doc is empty, obtain the URI used to request the
  // current page.  $args is an array of the argument keys and $vals is
  // the corresponding array of argument values.
  // If $trim is true, any previous args defined in $args are replaced.

  // Returns: The URI.

  // Start by building the core of the URI.
  
  $ret = build_uri($doc);

  if($trim && strstr($ret, '?'))
  {
    // Pull apart the argument string (if provided) and drop any args
    // that we're going to add back later.
    
    $chunk = preg_split('/\?/', $ret);
    $oldargs = preg_split('/&/', $chunk[1]);

    $ret = $chunk[0];

    $qst = 0;
    $amp = 0;

    foreach($oldargs as $a)
    {
      $as = preg_split('/=/', $a);

      if(!in_array($as[0], $args))
      {
	// No need to reencode $as since it presumably was already encoded.

	if(!$qst)
	{
	  $ret .= '?';
	  $qst++;
	}
	
	$ret .= ($amp ? '&' : '') . $as[0] . '=' . $as[1];

	if(!$amp)
	  $amp++;
      }
    }
  }
  
  if($args && $vals && (count($args) == count($vals)))
  {
    $amp = 0;

    if(!strstr($ret, '?'))
      $ret .= '?';
    else
      $amp = 1;

    for($i = 0;$i < count($args);$i++)
    {
      $ret .= ($amp ? '&' : '') . urlencode($args[$i]) . '='
	. urlencode($vals[$i]);

      $amp++;
    }
  }

  return($ret);
}

function cancel_meeting($mtgid)
{
  // Cancel the meeting $mtgid, sending notifications if appropriate.

  // Returns: true if fully successful, false otherwise.

  $ret = false;

  if($mtgid)
  {
    $oldstat = update_meeting_status($mtgid, "X");

    if($oldstat)
    {
      switch($oldstat)
      {
      case 'I':
	// Send un-invites
	send_invitations($mtgid);
	break;
      case 'S':
	// Send cancel notifications
	send_notifications($mtgid);
	break;
      }

      $ret = true;
    }
  }
  
  return($ret);
}

function change_meeting_location($mtgid, $locid)
{
  // Change the location of $mtgid and send notifications.

  // Returns: true if fully successful, false otherwise.

  global $dbc, $tx;

  if(update_meeting_location($mtgid, $locid, "C"))
  {
    // We only send notifications (as opposed to invitations) since
    // location shouldn't get set until the time has been established.

    send_notifications($mtgid, $tx['in.changed.loc']);
    return(true);
  }
  else
    return(false);
}

function clear_session_vars()
{
  // Clear the session vars used to track the login user.

  // Returns: true if auth is successful, false otherwise.
  
  // Variables set by auth handler
  unset($_SESSION['user']);
  unset($_SESSION['groups']);
  unset($_SESSION['mail']);

  // Variables set by mrsbs.inc
  unset($_SESSION['contactid']);
  unset($_SESSION['givenname']);
  unset($_SESSION['sn']);
  
  return(true);
}

function confirm_meeting_location($mtgid)
{
  // Confirm the location of $mtgid and send notifications.

  // Returns: true if fully successful, false otherwise.

  global $dbc, $tx;

  $sql = $dbc->Prepare("UPDATE mrsbs_meeting_info SET
                        locationstatus='C'
	                WHERE mtgid=" . $dbc->Param('a'));

  // We only send notifications (as opposed to invitations) since
  // location shouldn't get set until the time has been established.

  if($dbc->Execute($sql, array($mtgid)))
  {
    send_notifications($mtgid, $tx['in.confirmed.loc']);
    return(true);
  }
  else
    return(false);
}

function create_new_meeting($clone = -1)
{
  // Allocate a new meeting ID and add it to the database.
  // A session must be in place when calling this function.

  // Returns: The new meeting ID, or -1 on error.

  global $dbc;
  global $tx;
  
  $ret = -1;

  $mtgid = $dbc->GenID('mrsbs_mtgid_seq', 1001);
  
  $sql = $dbc->Prepare('INSERT INTO mrsbs_meeting_info
                        (mtgid, contactid, hostid, status, locationid,
                         locationstatus, replybymode)
                        VALUES (' . $dbc->Param('a') . ',' .
		        $dbc->Param('b') . ',' . $dbc->Param('c') . ',' .
		        $dbc->Param('d') . ',' . $dbc->Param('e') . ',' .
		        $dbc->Param('f') . ',' . $dbc->Param('g') . ')');
 
  $records = $dbc->Execute($sql, array($mtgid,
				       $_SESSION['contactid'],
				       $_SESSION['contactid'],
				       "B",
				       0,
				       "N",
				       "R"));

  if($records)
  {
    $ret = $mtgid;
    
    record_history($mtgid, $_SESSION['contactid'], "NEW", $tx['hi.new']);

    if($clone > -1)
    {
      // Clone the appropriate parts of the requested original meeting.
      // Errors here will not cause rollback.

      $srcmtg = get_meeting_info($clone);

      if($srcmtg)
      {
	// Check that contactid is still permitted to delegate for hostid

	if($srcmtg['hostid'] != $_SESSION['contactid']
	   && authorize_delegate("pcreate",
				 $_SESSION['contactid'],
				 $srcmtg['hostid']))
	  $nhostid = $srcmtg['hostid'];
	else
	  $nhostid = $_SESSION['contactid'];

	// Set locationid if location count is 0, or if locid is -1/-2

	if($srcmtg['locations']['count'] == 0 || $srcmtg['locationid'] < 0)
	{
	  $nlocid = $srcmtg['locationid'];
	  $nlocstat = $srcmtg['locationstatus'];
	}
	else
	{
	  $nlocid = 0;
	  $nlocstat = "N";
	}
	
	$sql = $dbc->Prepare('UPDATE mrsbs_meeting_info
                              SET hostid=' . $dbc->Param('a') . ',
                                  summary=' . $dbc->Param('b') . ',
                                  description=' . $dbc->Param('c') . ',
                                  duration=' . $dbc->Param('d') . ',
                                  replybymode=' . $dbc->Param('e') . ',
                                  locationid=' . $dbc->Param('f') . ',
                                  locationstatus=' . $dbc->Param('g') . '
                              WHERE mtgid=' . $dbc->Param('h'));
	
	if($dbc->Execute($sql, array($nhostid,
				     $srcmtg['summary'],
				     $srcmtg['description'],
				     $srcmtg['duration'],
				     $srcmtg['replybymode'],
				     $nlocid,
				     $nlocstat,
				     $mtgid)))
	{
	  // Populate invitee list

	  for($i = 0;$i < $srcmtg['invitees']['count'];$i++)
	  {
	    add_meeting_invitee($mtgid,
				$srcmtg['invitees'][$i]['contactid'],
				$srcmtg['invitees'][$i]['status']);
	  }

	  // Populate locations

	  for($i = 0;$i < $srcmtg['locations']['count'];$i++)
	  {
	    $sql = $dbc->Prepare('INSERT INTO mrsbs_potential_locations
                                  (locationid, mtgid, pref)
                                  VALUES (' . $dbc->Param('a') . ',' .
				  $dbc->Param('b') . ',' . $dbc->Param('c') .
				 ')');

	    $dbc->Execute($sql, $array($srcmtg['locations'][$i]['locationid'],
				       $mtgid,
				       $srcmtg['locations'][$i]['pref']));
	  }

	  // We populate windows only if their start time hasn't passed
	  
	  for($i = 0;$i < $srcmtg['windows']['count'];$i++)
	  {
	    if($srcmtg['windows'][$i]['begin'] > time())
	      add_meeting_window($mtgid,
				 $srcmtg['windows'][$i]['begin'],
				 $srcmtg['windows'][$i]['end']);
	  }
	  
	  // Record history cloned from $clone

	  record_history($mtgid, $_SESSION['contactid'], "CLND",
			 $tx['hi.clnd'] . $srcmtg['mtgid']);
	}
      }
    }
  }
  else
    print $dbc->ErrorMsg() . "<P>";

  return($ret);
}

function delete_contact($contactid)
{
  // Delete $contactid, deleting or updating all appropriate data.

  // Returns: true if fully successful, false otherwise.

  global $dbc;
  global $tx;

  if($contactid == $_SESSION['contactid'])
  {
    // Cannot remove self
    
    return(false);
  }
  
  $dbc->StartTrans();

  $ct = get_contact($contactid);

  if($ct)
  {
    // Set owned meetings to now be owned by their hosts

    $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info
                          SET contactid=hostid
                          WHERE contactid=' . $dbc->Param('a'));

    $dbc->Execute($sql, array($contactid));

    // Cancel meetings hosted if not already canceled
    // (check in update_meeting_status)

    foreach($ct['hosted'] as $c)
      cancel_meeting($c);

    // And set them to be owned by this admin or hosted by their new owner

    $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info
                          SET contactid=' . $dbc->Param('a') . ',
                              hostid=' . $dbc->Param('b') . '
                          WHERE hostid=' . $dbc->Param('c') . '
                          AND contactid=' . $dbc->Param('d'));
    
    $dbc->Execute($sql, array($_SESSION['contactid'],
			      $_SESSION['contactid'],
			      $contactid,
			      $contactid));

    foreach($ct['owned'] as $c)
      record_history($c, $contactid, "XOWN",
		     render_name("compact", $ct['givenname'],
				 $ct['sn'], $ct['mail']) . $tx['hi.xown']);

    $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info
                          SET hostid=contactid
                          WHERE hostid=' . $dbc->Param('a') . '
                          AND contactid!=' . $dbc->Param('b'));
    
    $dbc->Execute($sql, array($contactid,
			      $contactid));

    foreach($ct['hosted'] as $c)
      record_history($c, $contactid, "XHST",
		     render_name("compact", $ct['givenname'],
				 $ct['sn'], $ct['mail']) . $tx['hi.xhst']);

    // Remove any invitations and record that we did in the history
    
    $sql = $dbc->Prepare('DELETE FROM mrsbs_invitees
                          WHERE contactid=' . $dbc->Param('a'));
    
    $dbc->Execute($sql, array($contactid));

    foreach($ct['invited'] as $c)
      record_history($c, $contactid, "XINV",
		     render_name("compact", $ct['givenname'],
				 $ct['sn'], $ct['mail']) . $tx['hi.xinv']);

    // Remove delegations
    
    $sql = $dbc->Prepare('DELETE FROM mrsbs_delegates
                          WHERE contactid=' . $dbc->Param('a'));
    
    $dbc->Execute($sql, array($contactid));

    $sql = $dbc->Prepare('DELETE FROM mrsbs_delegates
                          WHERE delegateid=' . $dbc->Param('a'));

    $dbc->Execute($sql, array($contactid));

    // Set locations owned by this contact to the current user (admin)

    $sql = $dbc->Prepare('UPDATE mrsbs_locations
                          SET contactid=' . $dbc->Param('a') . '
                          WHERE contactid=' . $dbc->Param('b'));
    
    $dbc->Execute($sql, array($_SESSION['contactid'], $contactid));

    // Remove ACLs (as of this writing, though, there are no per-user ACLs)
    
    $sql = $dbc->Prepare('DELETE FROM mrsbs_acls
                          WHERE acltype="U"
                          AND who=' . $dbc->Param('a'));

    $dbc->Execute($sql, array($contactid));

    // Remove the main contact entry
    
    $sql = $dbc->Prepare('DELETE FROM mrsbs_contacts
                          WHERE contactid=' . $dbc->Param('a'));

    $dbc->Execute($sql, array($contactid));
  }
  else
    $dbc->FailTrans();

  return($dbc->CompleteTrans());
}

function determine_availability($windows, $begin, $end)
{
  // Determine availability for the window $begin to $end based on $windows,
  // an array of the form of the 'windows' element returned by get_invite_info.

  // Returns:
  //  u: unknown
  //  p: preferred
  //  a: available
  //  P: not preferred
  //  A: not available

  $ret = 'u';

  for($i = 0;$i < $windows['count'];$i++)
  {
    // Iterate through the whole array since there might be multiple
    // relevant values.

    if(($windows[$i]['begin'] <= $begin
	&& $windows[$i]['end'] > $begin)
       ||
       ($windows[$i]['begin'] < $end
	&& $windows[$i]['end'] >= $end)
       ||
       ($windows[$i]['begin'] >= $begin
	&& $windows[$i]['end'] <= $end))
    {
      // Window matches if it crosses begin, crosses end, or is entirely
      // within begin and end.  Return the most restrictive status we see.
      
      switch($windows[$i]['status'])
      {
      case 'A':
	$ret = 'A';
	break;
      case 'P':
	if($ret != 'A')
	  $ret = 'P';
	break;
      case 'a':
	if($ret != 'A' && $ret != 'P')
	  $ret = 'a';
	break;
      case 'p':
	if($ret == 'u')
	  $ret = 'p';
	break;
      }
    }
  }
  
  return($ret);
}

function directory_query($search)
{
  // Query the directory with search string $search.

  // Returns: false on error, or an array of the form
  //  $ret['count']: Number of entries
  //  $ret[#]['dn']: DN for entry #
  //  $ret[#][field][##]: Attribute field value ## for entry #

  global $config;
  
  $ret = false;
  
  if($config['ldaphost'] != "")
  {
    $ds = ldap_connect($config['ldaphost'], $config['ldapport']);

    if($ds)
    {
      $r = ldap_bind($ds);

      if($r)
      {
	// If $search has an @, we were given an email address

	if(strchr($search, '@'))
	{
	  $sr = ldap_search($ds, $config['ldapbase'], "mail=".$search);
	
	  if($sr)
	    $ret = ldap_get_entries($ds, $sr);
	}
	else
	{
	  // We'll try several searches, until one matches.  We perform
	  // a very similar search in search_contacts().

	  // Perhaps we should try many/all searches and return all
	  // potential matches, but maybe not try soundex unless
	  // equality fails

	  // First, we try simply equality, then we try soundex

	  foreach(array('=', '~=') as $equality)
	  {
	    // First, we try to match all tokens, then any token

	    if($config['contactsearchOR'])
	      $sarray = array('&', '|');
	    else
	      $sarray = array('&');
	    
	    foreach($sarray as $op)
	    {
	      // First, we try just the tokens.  Then, we try wildcarding them.
	      
	      foreach(array('', '*') as $wildcard)
	      {
		$tokens = explode(" ", $search);

		$n = count($tokens) - 1;

		// Iterate through any "middle" tokens, trying them in both
		// givenname and sn.  Eg, for James Michael Patrick Smith
		// we would try (James Michael Patrick) (Smith),
		// (James Michael) (Patrick Smith). and
		// (James) (Michael Patrick Smith).
	      
		if($n == 0)  // Single tokens still need to be searched on
		  $n = 1;
	      
		for($i = 0;$i < $n;$i++)
		{
		  // Try set1 as gn, set2 as sn
		  
		  $filter = "(".$op;
		  
		  for($j = 0;$j <= $i;$j++)
		    $filter .= "(givenname" . $equality . $tokens[$j]
		      . $wildcard . ")";
	      
		  for($j = $i+1;$j <= $n;$j++)
		    $filter .= "(sn" . $equality . $tokens[$j].$wildcard . ")";

		  $filter .= ")";
		  
		  $sr = ldap_search($ds, $config['ldapbase'], $filter);

		  if($sr && ldap_count_entries($ds, $sr) > 0)
		  {
		    $ret = ldap_get_entries($ds, $sr);
		    break 4;  // Break out of all the loops
		  }
		
		  // Try set1 as sn, set2 as gn
		
		  $filter = "(".$op;
		  
		  for($j = 0;$j <= $i;$j++)
		    $filter .= "(sn" . $equality . $tokens[$j].$wildcard . ")";
	      
		  for($j = $i+1;$j <= $n;$j++)
		    $filter .= "(givenname" . $equality . $tokens[$j]
		      . $wildcard . ")";

		  $filter .= ")";
		  
		  $sr = ldap_search($ds, $config['ldapbase'], $filter);
		  
		  if($sr && ldap_count_entries($ds, $sr) > 0)
		  {
		    $ret = ldap_get_entries($ds, $sr);
		    break 4;  // Break out of all the loops
		  }
		}
	      }
	    }
	  }
	}
      }

      ldap_close($ds);
    }
  }
  else
  {
    $ret = array();
    $ret['count'] = 0;
  }

  return($ret);
}

function generate_ics($mtg, $i)
{
  // Generate an ICS invite for $mtg (as returned by get_meeting_info).
  // If $i is > -1, attach a reference to the reply URI.

  // Returns: The text of the invite, or empty text on error.

  global $config, $tx;

  $ics = "";
  
  // Create a new VCALENDAR
  $ical = new iCalendar;

  // Identify the application
  $ical->add_property('prodid', '-//Paul Edwards//Meeting Request Scheduling and Booking System//EN');
  
  $ical->add_property('method', 'REQUEST');
	
  //----- Create a new VEVENT
  $event = new iCalendar_event;

  // RFC2445 says this SHOULD be RFC822 style, so hide@address.com
  // This should correlate to the database key for this event We
  // have to use $config['servername'] because we may be running
  // from cron so $_SERVER isn't set.
      
  $u = $mtg['mtgid'] . "@" . $config['servername'];
  $event->add_property('uid', $u);
	
  // The time this event was created, MUST be in UTC time format
  // YYYYMMDDTHHMMSSZ

  $event->add_property('dtstamp', gmdate("Ymd\THis\Z"));

  // Start and end time of the event, we'll use UTC time as above
  // for consistency
  
  $event->add_property('dtstart', gmdate("Ymd\THis\Z",
					 $mtg['scheduledfor']));
  $event->add_property('dtend',
		       gmdate("Ymd\THis\Z",
			      $mtg['scheduledfor'] + ($mtg['duration'] * 60)));

  // This is a private event, could perhaps be PUBLIC or CONFIDENTIAL
      
  $event->add_property('class', 'PRIVATE');

  // Add a reference to the application

  if($i > -1)
    $event->add_property('attach',
			 build_uri('/reply/status.php?inviteid='
				   . $mtg['invitees'][$i]['inviteid']));
  else
    $event->add_property('attach',
			 build_uri('/schedule/review.php?mtgid='
				   . $mtg['mtgid']));
      
  // Summary (short) and description (long)

  $event->add_property('summary', rfc2445_text($mtg['summary']));
  $event->add_property('description', rfc2445_text($mtg['description']));
    
  // Status of the event: TENTATIVE, CONFIRMED, or CANCELLED

  if($mtg['status'] == 'S')
    $event->add_property('status', 'CONFIRMED');
  else
    $event->add_property('status', 'CANCELLED');

  // Organizer (host) of the event
      
  $event->add_property('organizer',
		       'mailto:' . $mtg['hostmail'] ,
		       array('cn' => render_name("compact",
						 $mtg['hostgivenname'],
						 $mtg['hostsn'],
						 $mtg['hostmail'])));
    
  // Who is attending.  We skip anyone who explicitly declined.
      
  // role can be CHAIR, REQ-PARTICIPANT, OPT-PARTICIPANT, NON-PARTICIPANT
  // cutype can be INDIVIDUAL, GROUP, RESOURCE, ROOM, UNKNOWN
  // could add 'rsvp'-> TRUE|FALSE to indicate if reply is required
  $event->add_property('attendee',
		       'mailto:' . $mtg['hostmail'] ,
		       array('cn' => render_name("compact",
						 $mtg['hostgivenname'],
						 $mtg['hostsn'],
						 $mtg['hostmail']),
			     'cutype' => 'INDIVIDUAL',
			     'role' => 'CHAIR'));

  for($j = 0;$j < $mtg['invitees']['count'];$j++)
    if($mtg['invitees'][$j]['reply'] != 'D')
    {
      $role = "";
      
      switch($mtg['invitees'][$j]['status'])
      {
      case 'R':
	$role = "REQ-PARTICIPANT";
	break;
      case 'O':
	$role = "OPT-PARTICIPANT";
	break;
      case 'N':
	$role = "NON-PARTICIPANT";
	break;
      }
	  
      $event->add_property('attendee',
			   'mailto:' . $mtg['invitees'][$j]['mail'],
			   array('cn' => render_name("compact",
						     $mtg['invitees'][$j]['givenname'],
						     $mtg['invitees'][$j]['sn'],
						     $mtg['invitees'][$j]['mail']),
				 'cutype' => 'INDIVIDUAL',
				 'role' => $role));
    }

  // Location, if known

  $event->add_property('location',
		       render_location($mtg['location'],
				       $mtg['locinfo'],
				       $mtg['locationstatus']));

  // Attach the VEVENT to the VCALENDAR
  
  $ical->add_component($event);
  $ics = $ical->serialize();

  return($ics);
}

function get_acls($rtype, $rid)
{
  // Obtain the ACLs for the resource of type $rtype and id $rid.

  // Returns: false on error, or an array of the form
  //  $a['a']: Numeric permission for "all"
  //  $a['g']['count']: Number of group permissions
  //  $a['g'][#]['who']: Name of group permission #
  //  $a['g'][#]['perm']: Numeric permission for #
  //  $a['u']['count']: Number of user permissions
  //  $a['u'][#]['who']: Name of user permission #
  //  $a['u'][#]['perm']: Numeric permission for #

  global $dbc;

  $ret = false;

  $sql = $dbc->Prepare("SELECT acltype, who, perm
                        FROM mrsbs_acls
                        WHERE resourcetype=" . $dbc->Param('a') . "
                        AND resourceid=" . $dbc->Param('b'));

  $records = $dbc->Execute($sql, array($rtype, $rid));

  if($records)
  {
    $ret = array();

    $ret['g'] = array();
    $ret['g']['count'] = 0;
    $ret['u'] = array();
    $ret['u']['count'] = 0;
    
    while(!$records->EOF)
    {
      switch($records->fields['acltype'])
      {
      case "A":
	$ret['a'] = $records->fields['perm'];
	break;
      case "G":
      case "U":
	$a = strtolower($records->fields['acltype']);
	$i = $ret[$a]['count'];
	$ret[$a][$i] = array();
	$ret[$a][$i]['perm'] = $records->fields['perm'];
	$ret[$a][$i]['who'] = hsstr($records->fields['who']);
	$ret[$a]['count']++;
	break;
      default:
	break;
      }
  
      $records->MoveNext();
    }
  }
  
  return($ret);
}

function get_contact($contactid)
{
  // Obtain information about contact $contactid.

  // Returns: false on error, or an array of the form
  //  $ret['contactid']: Contact ID
  //  $ret['mail']: Email
  //  $ret['givenname']: Given name
  //  $ret['sn']: Surname
  //  $ret['delegate']: Array of contacts this contact is a delegate for, or false
  //  $ret['delegator']: Array of contacts this contact delegates to, or false
  //  $ret['owned']: Array of meeting IDs owned by this contact, or false
  //  $ret['hosted']: Array of meeting IDs hosted by this contact, or false
  //  $ret['invited']: Array of meeting IDs this contact is invited to, or false
  //  $ret['locations']: Array of location IDs owned by this contact, or false

  global $dbc;

  // General info

  $sql = $dbc->Prepare("SELECT * FROM mrsbs_contacts
                        WHERE contactid=" . $dbc->Param('a'));
  
  $ret = $dbc->GetRow($sql, array($contactid));

  if($ret)
  {
    $ret['mail'] = hsstr($ret['mail']);
    $ret['givenname'] = hsstr($ret['givenname']);
    $ret['sn'] = hsstr($ret['sn']);
    
    $sql = $dbc->Prepare("SELECT contactid FROM mrsbs_delegates
                          WHERE delegateid=" . $dbc->Param('a'));
      
    $ret['delegate'] = $dbc->GetCol($sql, array($contactid));
    
    $sql = $dbc->Prepare("SELECT delegateid FROM mrsbs_delegates
                          WHERE contactid=" . $dbc->Param('a'));
      
    $ret['delegator'] = $dbc->GetCol($sql, array($contactid));

    // binary indicates case sensitive comparisons, we want non-canceled
    $sql = $dbc->Prepare("SELECT mtgid FROM mrsbs_meeting_info
                          WHERE contactid=" . $dbc->Param('a') . "
			  AND hostid!=" . $dbc->Param('b') . "
                          AND status >= BINARY 'A'
                          AND status <= BINARY 'Z'");
      
    $ret['owned'] = $dbc->GetCol($sql, array($contactid, $contactid));
    
    $sql = $dbc->Prepare("SELECT mtgid FROM mrsbs_meeting_info
                          WHERE hostid=" . $dbc->Param('a') . "
                          AND status >= BINARY 'A'
                          AND status <= BINARY 'Z'");
      
    $ret['hosted'] = $dbc->GetCol($sql, array($contactid));
    
    $sql = $dbc->Prepare("SELECT mtgid FROM mrsbs_invitees
                          WHERE contactid=" . $dbc->Param('a'));
    
    $ret['invited'] = $dbc->GetCol($sql, array($contactid));
    
    $sql = $dbc->Prepare("SELECT locationid FROM mrsbs_locations
                          WHERE contactid=" . $dbc->Param('a'));
      
    $ret['locations'] = $dbc->GetCol($sql, array($contactid));
  }
  
  return($ret);
}

function get_contacts()
{
  // Obtain all known contacts.

  // Returns: false on error, or an array of the form
  //  $ret['count']: # of locations
  //  $ret[#]['contactid']: ID of #
  //  $ret[#]['mail']: Email address of #
  //  $ret[#]['givenname']: Givenname of #
  //  $ret[#]['sn']: Surname of #

  global $dbc;

  $ret = false;

  // General info
  
  $sql = "SELECT * FROM mrsbs_contacts ORDER BY sn ASC, givenname ASC";

  $records = $dbc->Execute($sql);

  if($records)
  {
    $ret = array();
    $ret['count'] = $records->RecordCount();
    $i = 0;
    
    while(!$records->EOF)
    {
      $ret[$i] = array();
      $ret[$i]['contactid'] = $records->fields['contactid'];
      $ret[$i]['mail'] = hsstr($records->fields['mail']);
      $ret[$i]['givenname'] = hsstr($records->fields['givenname']);
      $ret[$i]['sn'] = hsstr($records->fields['sn']);
  
      $records->MoveNext();
      $i++;
    }
  }
  else
    $ret['count'] = 0;

  return($ret);
}

function get_delegate_create_for($contactid)
{
  // Obtain the contact information for whom $contactid may create meetings.

  // Returns: false on error, or an array of the form
  //  $ret['count']: # of contacts
  //  $ret[#]['contactid']: Contact ID of contact #
  //  $ret[#]['mail']: Contact ID of contact #
  //  $ret[#]['givenname']: Given name of contact #
  //  $ret[#]['sn']: Surname of contact #

  global $dbc;

  $ret = false;

  $sql = $dbc->Prepare("SELECT mrsbs_contacts.contactid,
                               mrsbs_contacts.givenname,
                               mrsbs_contacts.sn,
                               mrsbs_contacts.mail
                        FROM mrsbs_contacts
                        INNER JOIN mrsbs_delegates
                        ON mrsbs_contacts.contactid=mrsbs_delegates.delegateid
                        WHERE delegateid=" . $dbc->Param('a') . "
                        AND pcreate='Y'");

  $records = $dbc->Execute($sql, array($contactid));

  if($records)
  {
    $ret = array();
    $ret['count'] = $records->RecordCount();
    $i = 0;
    
    while(!$records->EOF)
    {
      $ret[$i] = array();
      $ret[$i]['contactid'] = $records->fields['contactid'];
      $ret[$i]['mail'] = hsstr($records->fields['mail']);
      $ret[$i]['givenname'] = hsstr($records->fields['givenname']);
      $ret[$i]['sn'] = hsstr($records->fields['sn']);
  
      $records->MoveNext();
      $i++;
    }
  }
  
  return($ret);
}

function get_delegate_reply_for($contactid)
{
  // Obtain the delegate information for those who may reply to meetings
  // on behalf of $contactid.

  // Returns: false on error, or an array of the form
  //  $ret['count']: # of contacts
  //  $ret[#]['contactid']: Contact ID of contact #
  //  $ret[#]['mail']: Contact ID of contact #
  //  $ret[#]['givenname']: Given name of contact #
  //  $ret[#]['sn']: Surname of contact #

  global $dbc;

  $ret = false;

  $sql = $dbc->Prepare("SELECT mrsbs_contacts.contactid,
                               mrsbs_contacts.givenname,
                               mrsbs_contacts.sn,
                               mrsbs_contacts.mail
                        FROM mrsbs_contacts
                        INNER JOIN mrsbs_delegates
                        ON mrsbs_contacts.contactid=mrsbs_delegates.delegateid
                        WHERE mrsbs_delegates.contactid=" . $dbc->Param('a') . "
                        AND preply='Y'");

  $records = $dbc->Execute($sql, array($contactid));

  if($records)
  {
    $ret = array();
    $ret['count'] = $records->RecordCount();
    $i = 0;
    
    while(!$records->EOF)
    {
      $ret[$i] = array();
      $ret[$i]['contactid'] = $records->fields['contactid'];
      $ret[$i]['mail'] = hsstr($records->fields['mail']);
      $ret[$i]['givenname'] = hsstr($records->fields['givenname']);
      $ret[$i]['sn'] = hsstr($records->fields['sn']);
  
      $records->MoveNext();
      $i++;
    }
  }
  
  return($ret);
}

function get_delegates()
{
  // Obtain information about all known delegates.

  // Returns: false on error, or an array of the form
  //  $ret['count']: Number of matching delegates
  //  $ret[#]['contactid']: Contact ID of this delegator
  //  $ret[#]['contactgivenname']: Given name of this delegator
  //  $ret[#]['contactsn']: Surname of this delegator
  //  $ret[#]['contactmail']: Mail of this delegator
  //  $ret[#]['delegateid']: Contact ID of this delegate
  //  $ret[#]['delegategivenname']: Given name of this delegate
  //  $ret[#]['delegatesn']: Surname of this delegate
  //  $ret[#]['delegatemail']: Mail of this delegate
  //  $ret[#]['create']: true if delegate can create meetings for contact
  //  $ret[#]['reply']: true if delegate can reply to meetings for contact

  global $dbc;
  $ret = false;
  
  $sql = "SELECT mrsbs_delegates.*,
                 mrsbs_contacts.givenname AS contactgivenname,
                 mrsbs_contacts.sn AS contactsn,
                 mrsbs_contacts.mail AS contactmail,
                 m2.givenname AS delegategivenname,
                 m2.sn AS delegatesn,
                 m2.mail AS delegatemail
          FROM mrsbs_delegates
          LEFT OUTER JOIN mrsbs_contacts
          ON mrsbs_contacts.contactid=mrsbs_delegates.contactid
          LEFT OUTER JOIN mrsbs_contacts AS m2
          ON m2.contactid=mrsbs_delegates.delegateid
          ORDER BY mrsbs_contacts.sn ASC";

  $records = $dbc->Execute($sql);

  if($records)
  {
    $ret['count'] = $records->RecordCount();
    $i = 0;
    
    while(!$records->EOF)
    {
      $ret[$i] = array();
      $ret[$i]['contactid'] = $records->fields['contactid'];
      $ret[$i]['contactgivenname'] = hsstr($records->fields['contactgivenname']);
      $ret[$i]['contactsn'] = hsstr($records->fields['contactsn']);
      $ret[$i]['contactmail'] = hsstr($records->fields['contactmail']);
      $ret[$i]['delegateid'] = $records->fields['delegateid'];
      $ret[$i]['delegategivenname'] = hsstr($records->fields['delegategivenname']);
      $ret[$i]['delegatesn'] = hsstr($records->fields['delegatesn']);
      $ret[$i]['delegatemail'] = hsstr($records->fields['delegatemail']);
      $ret[$i]['create'] = ($records->fields['pcreate'] == 'Y' ? true : false);
      $ret[$i]['reply'] = ($records->fields['preply'] == 'Y' ? true : false);
      
      $records->MoveNext();
      $i++;
    }
  }

  return($ret);
}

function get_invite_info($inviteid, $windowsort="pref")
{
  // Obtain the information associated with $inviteid, sorting windows by
  // $windowsort.

  // Returns: false on error, or an array of the form
  //  $ret['inviteid']: Invite ID
  //  $ret['mtgid']: Meeting ID
  //  $ret['contactid']: Contact ID of invitee
  //  $ret['givenname']: Given name of invitee
  //  $ret['sn']: Surname of invitee
  //  $ret['mail']: Email of invitee
  //  $ret['status']: [R]equired, [O]ptional, [N]on-Attending
  //  $ret['reply']: [A]ttending, [D]eclining, [N]one
  //  $ret['locations'][locid]['pref']: 0=no, 1=ok, 2=preferred for locid
  //  $ret['windows']['count']: Number of reply windows
  //  $ret['windows'][#]['begin']: Begin time of window #
  //  $ret['windows'][#]['end']: End time of window #
  //  $ret['windows'][#]['status']: [p]ref, [a]vail, not [P]ref/[A]vail for #
  //  $ret['mtg']: Array of the form returned by get_meeting_info()

  global $dbc;

  // General info

  $sql = $dbc->Prepare("SELECT mrsbs_invitees.*,
                               mrsbs_contacts.givenname,
                               mrsbs_contacts.sn,
                               mrsbs_contacts.mail
                        FROM mrsbs_invitees
                        LEFT OUTER JOIN mrsbs_contacts
                        ON mrsbs_invitees.contactid=mrsbs_contacts.contactid
                        WHERE inviteid=" . $dbc->Param('a'));

  $ret = $dbc->GetRow($sql, array($inviteid));
  
  if($ret)
  {
    $ret['givenname'] = hsstr($ret['givenname']);
    $ret['sn'] = hsstr($ret['sn']);
    $ret['mail'] = hsstr($ret['mail']);
    
    $ret['mtg'] = get_meeting_info($ret['mtgid'], $windowsort);

    $ret['locations'] = array();
    $ret['locations']['count'] = 0;

    $sql = $dbc->Prepare("SELECT locationid, pref
                          FROM mrsbs_reply_locations
                          WHERE inviteid=" . $dbc->Param('a'));

    $records = $dbc->Execute($sql, array($inviteid));

    if($records)
    {
      while(!$records->EOF)
      {
	$ret['locations'][$records->fields['locationid']]['pref'] =
	  $records->fields['pref'];
	
	$records->MoveNext();
      }
    }

    $ret['windows'] = array();
    $ret['windows']['count'] = 0;

    $sql = $dbc->Prepare("SELECT status, begin, end
                          FROM mrsbs_reply_windows
                          WHERE inviteid=" . $dbc->Param('a'));

    $records = $dbc->Execute($sql, array($inviteid));

    if($records)
    {
      $ret['windows']['count'] = $records->RecordCount();
      $i = 0;
    
      while(!$records->EOF)
      {
	$ret['windows'][$i] = array();
	$ret['windows'][$i]['status'] = $records->fields['status'];
	$ret['windows'][$i]['begin'] =
	  $dbc->UnixTimeStamp($records->fields['begin']);
	$ret['windows'][$i]['end'] =
	  $dbc->UnixTimeStamp($records->fields['end']);
	
	$records->MoveNext();
	$i++;
      }
    }
  }

  return($ret);
}

function get_inviteid($mtgid, $contactid)
{
  // Obtain the inviteid for the invitation of $contactid to $mtgid.

  // Returns: The inviteid, or false on error.

  global $dbc;

  $sql = $dbc->Prepare("SELECT inviteid FROM mrsbs_invitees
                        WHERE mtgid=" . $dbc->Param('a') . "
                        AND contactid=" . $dbc->Param('b'));
  
  return($dbc->GetOne($sql, array($mtgid, $contactid)));
}

function get_location($locid)
{
  // Obtain information about location $locid.

  // Returns: false on error, or an array of the form
  //  $ret['locationid']: ID
  //  $ret['description']: Description
  //  $ret['capacity']: Capacity
  //  $ret['contactid']: Contact ID who owns location
  //  $ret['givenname']: Given name of this meeting's owner
  //  $ret['sn']: Surname of this meeting's owner
  //  $ret['mail']: Email of this meeting's owner
  //  $ret['system']: [I]nternal or [M]RBS
  //  $ret['acls']: ACLs for this location, in the form returned by get_acls

  global $dbc;

  // General info

  $sql = $dbc->Prepare("SELECT mrsbs_locations.*,
                               mrsbs_contacts.givenname,
                               mrsbs_contacts.sn,
                               mrsbs_contacts.mail
                        FROM mrsbs_locations
                        LEFT OUTER JOIN mrsbs_contacts
                        ON mrsbs_locations.contactid=mrsbs_contacts.contactid
                        WHERE locationid=" . $dbc->Param('a'));
  
  $ret = $dbc->GetRow($sql, array($locid));

  if($ret)
  {
    $ret['description'] = hsstr($ret['description']);
    $ret['givenname'] = hsstr($ret['givenname']);
    $ret['sn'] = hsstr($ret['sn']);
    $ret['mail'] = hsstr($ret['mail']);
    
    $ret['acls'] = get_acls("L", $locid);
  }

  return($ret);
}

function get_location_owner($locationid)
{
  // Obtain the contactid who owns $locationid.

  // Returns: The contactid, or false on error.

  global $dbc;

  $sql = $dbc->Prepare("SELECT contactid FROM mrsbs_locations
                        WHERE locationid=" . $dbc->Param('a'));
  
  return($dbc->GetOne($sql, array($locationid)));
}

function get_locations()
{
  // Obtain all known locations.

  // Returns: false on error, or an array of the form
  //  $ret['count']: # of locations
  //  $ret[#]['locationid']: ID of #
  //  $ret[#]['description']: Description of #
  //  $ret[#]['capacity']: Capacity of #
  //  $ret[#]['contactid']: Contact ID who owns #
  //  $ret[#]['system']: [I]nternal or [M]RBS

  global $dbc;

  $ret = false;

  // General info
  
  $sql = "SELECT * FROM mrsbs_locations";

  $records = $dbc->Execute($sql);

  if($records)
  {
    $ret = array();
    $ret['count'] = $records->RecordCount();
    $i = 0;
    
    while(!$records->EOF)
    {
      $ret[$i] = array();
      $ret[$i]['locationid'] = $records->fields['locationid'];
      $ret[$i]['description'] = hsstr($records->fields['description']);
      $ret[$i]['capacity'] = $records->fields['capacity'];
      $ret[$i]['contactid'] = $records->fields['contactid'];
      $ret[$i]['system'] = $records->fields['system'];
  
      $records->MoveNext();
      $i++;
    }
  }
  else
    $ret['count'] = 0;

  return($ret);
}

function get_meeting_history($mtgid)
{
  // Obtain the history for meeting $mtgid.

  // Returns: false on error, or an array of the form
  //  $ret['mtgid']: Meeting ID
  //  $ret['count']: # of records
  //  $ret[#]['contactid']: Contact ID for record #
  //  $ret[#]['contactgivenname']: Given name for record #'s contact
  //  $ret[#]['contactsn']: Surname for record #'s contact
  //  $ret[#]['contactmail']: Mail for record #'s contact
  //  $ret[#]['rtime']: Time of record #
  //  $ret[#]['action']: Action code of record #
  //  $ret[#]['desc']: Description of record #

  global $dbc;

  $ret = false;

  $sql = $dbc->Prepare("SELECT mrsbs_history.mtgid,
                               mrsbs_history.contactid,
                               mrsbs_history.rtime,
                               mrsbs_history.action,
                               mrsbs_history.description,
                               mrsbs_contacts.givenname,
                               mrsbs_contacts.sn,
                               mrsbs_contacts.mail
                        FROM mrsbs_history
                        LEFT OUTER JOIN mrsbs_contacts
                        ON mrsbs_contacts.contactid=mrsbs_history.contactid
                        WHERE mtgid=" . $dbc->Param('a') . "
                        ORDER BY rtime DESC");
	  
  $records = $dbc->Execute($sql, array($mtgid));

  if($records)
  {
    $ret = array();
    $ret['mtgid'] = $mtgid;
    $ret['count'] = $records->RecordCount();
    $i = 0;
    
    while(!$records->EOF)
    {
      $ret[$i] = array();
      $ret[$i]['mtgid'] = $records->fields['mtgid'];
      $ret[$i]['contactid'] = $records->fields['contactid'];
      $ret[$i]['contactgivenname'] = hsstr($records->fields['givenname']);
      $ret[$i]['contactsn'] = hsstr($records->fields['sn']);
      $ret[$i]['contactmail'] = hsstr($records->fields['mail']);
      $ret[$i]['rtime'] = $dbc->UnixTimeStamp($records->fields['rtime']);
      $ret[$i]['action'] = $records->fields['action'];
      $ret[$i]['desc'] = hsstr($records->fields['description']);
  
      $records->MoveNext();
      $i++;
    }
  }
  else
    $ret['count'] = 0;

  return($ret);
}

function get_meeting_host($mtgid)
{
  // Obtain the contactid who hosts $mtgid.

  // Returns: The contactid, or false on error.

  global $dbc;

  $sql = $dbc->Prepare("SELECT hostid FROM mrsbs_meeting_info
                        WHERE mtgid=" . $dbc->Param('a'));

  return($dbc->GetOne($sql, array($mtgid)));
}

function get_meeting_ids()
{
  // Obtain all valid meeting IDs.

  // Returns: An array of all meeting IDs.

  global $dbc;

  $sql = "SELECT mrsbs_meeting_info.mtgid
            FROM mrsbs_meeting_info";
    
  return($dbc->GetCol($sql, false));
}

function get_meeting_ids_contactid_own_host($contactid)
{
  // Obtain the meeting IDs where owns or hosts the meeting.

  // Returns: An array of matching meeting IDs.

  global $dbc;
  $ret = array();

  if($contactid > -1)
  {
    $sqlargs = array();
    
    $sql = "SELECT mrsbs_meeting_info.mtgid
            FROM mrsbs_meeting_info
            WHERE (mrsbs_meeting_info.contactid="  . $dbc->Param('a') . "
            OR mrsbs_meeting_info.hostid=" . $dbc->Param('b') . "
            OR mrsbs_meeting_info.mtgid IN
             (SELECT mrsbs_invitees.mtgid
              FROM mrsbs_invitees
              WHERE mrsbs_invitees.contactid=" . $dbc->Param('c') . "));
           ";
       
    array_push($sqlargs, $contactid, $contactid, $contactid);

    $ret = $dbc->GetCol($sql, (count($sqlargs) > 0 ? $sqlargs : false));
  }
  
  return($ret);
}

function get_meeting_ids_string_match($searchtext, $field = "")
{
  // Obtain the meeting IDs where $searchtext matches $field if specified,
  // otherwise any text field.  For advanced searches where $field is
  // "duration", "replyby", or "start", $searchtext is an array of the
  // appropriate components.

  // Returns: An array of matching meeting IDs.

  global $dbc;
  $ret = array();

  if($searchtext != "")
  {
    $sqlargs = array();
    
    $sql = "SELECT mrsbs_meeting_info.mtgid
            FROM mrsbs_meeting_info
            WHERE (";

    // Search summary text

    if($field == "" || strcasecmp($field, "summary")==0)
    {
      $sql .= "mrsbs_meeting_info.summary LIKE " . $dbc->Param('a');
      array_push($sqlargs, '%'.$searchtext.'%');
    }

    // Search description text
    
    if($field == "" || strcasecmp($field, "description")==0)
    {
      if($field == "") { $sql .= " OR "; }
      $sql .= "mrsbs_meeting_info.description LIKE " . $dbc->Param('b');
      array_push($sqlargs, '%'.$searchtext.'%');
    }

    // Search meeting owner

    if($field == "" || strcasecmp($field, "owner")==0)
    {
      if($field == "") { $sql .= " OR "; }
      $sql .= "mrsbs_meeting_info.contactid IN
               (SELECT mrsbs_contacts.contactid
                FROM mrsbs_contacts
                WHERE mrsbs_contacts.mail LIKE " . $dbc->Param('c') . "
                OR mrsbs_contacts.givenname LIKE " . $dbc->Param('d') . "
                OR mrsbs_contacts.sn LIKE " . $dbc->Param('e') . "
               )";
      array_push($sqlargs, '%'.$searchtext.'%',
		 '%'.$searchtext.'%', '%'.$searchtext.'%');
    }
    
    // Search meeting host

    if($field == "" || strcasecmp($field, "host")==0)
    {
      if($field == "") { $sql .= " OR "; }
      $sql .= "mrsbs_meeting_info.hostid IN
               (SELECT mrsbs_contacts.contactid
                FROM mrsbs_contacts
                WHERE mrsbs_contacts.mail LIKE " . $dbc->Param('f') . "
                OR mrsbs_contacts.givenname LIKE " . $dbc->Param('g') . "
                OR mrsbs_contacts.sn LIKE " . $dbc->Param('h') . "
               )";
      array_push($sqlargs, '%'.$searchtext.'%',
		 '%'.$searchtext.'%', '%'.$searchtext.'%');
    }
    
    // Search invitees

    if($field == "" || strcasecmp($field, "invitees")==0)
    {
      if($field == "") { $sql .= " OR "; }
      $sql .= "mrsbs_meeting_info.mtgid IN
               (SELECT mrsbs_invitees.mtgid
                FROM mrsbs_invitees
                WHERE mrsbs_invitees.contactid IN
                (SELECT mrsbs_contacts.contactid
                 FROM mrsbs_contacts
                 WHERE mrsbs_contacts.mail LIKE " . $dbc->Param('i') . "
                 OR mrsbs_contacts.givenname LIKE " . $dbc->Param('j') . "
                 OR mrsbs_contacts.sn LIKE " . $dbc->Param('k') . "
                )
               )";
      array_push($sqlargs, '%'.$searchtext.'%',
		 '%'.$searchtext.'%', '%'.$searchtext.'%');
    }

    // Search location
    
    if($field == "" || strcasecmp($field, "location")==0)
    {
      if($field == "") { $sql .= " OR "; }
      $sql .= "mrsbs_meeting_info.locationid IN
               (SELECT mrsbs_locations.locationid
                FROM mrsbs_locations
                WHERE description LIKE " . $dbc->Param('l') . "
               )";
      array_push($sqlargs, '%'.$searchtext.'%');
    }

    // Search by meeting ID
    
    if($field == "" || strcasecmp($field, "mtgid")==0)
    {
      if($field == "") { $sql .= " OR "; }
      $sql .= "mrsbs_meeting_info.mtgid = " . $dbc->Param('m');
      // If copying and pasting, NOTE this searchtext is NOT wildcarded
      // ('=', not 'LIKE').
      array_push($sqlargs, $searchtext);
    }

    if($field != "")
    {
      // These should ONLY be searched if requested (ie: advanced, not
      // simple).
      
      if(strcasecmp($field, "status")==0)
      {
	if($searchtext == "X")
	  $sql .= "mrsbs_meeting_info.status >= BINARY 'a'
                   AND status <= BINARY 'z'";
	else
	{
	  $sql .= "mrsbs_meeting_info.status = " . $dbc->Param('n');
	  array_push($sqlargs, $searchtext);
	}
      }

      if(strcasecmp($field, "duration")==0)
      {
	// $searchtext is multiple components.

	$sql .= "mrsbs_meeting_info.duration ";

	if($searchtext['comp'] == 'lt')
	  $sql .= "< ";
	elseif($searchtext['comp'] == 'eq')
	  $sql .= "= ";
	elseif($searchtext['comp'] == 'gt')
	  $sql .= "> ";
	
	// Calculate duration in minutes.

	$dur = $searchtext['val'];

	if($searchtext['unit'] == 'd')
	  $dur *= 60 * 24;
	elseif($searchtext['unit'] == 'h')
	  $dur *= 60;

	$sql .= $dbc->Param('o');
	array_push($sqlargs, $dur);
      }
      
      if(strcasecmp($field, "replyby")==0)
      {
	// $searchtext is multiple components.

	$sql .= "mrsbs_meeting_info.replyby ";

	if($searchtext['comp'] == 'lt')
	  $sql .= "< ";
	elseif($searchtext['comp'] == 'eq')
	  $sql .= "= ";
	elseif($searchtext['comp'] == 'gt')
	  $sql .= "> ";

	$sql .= $dbc->DBTimeStamp($searchtext['val']);
      }
      
      if(strcasecmp($field, "start")==0)
      {
	// $searchtext is multiple components.

	$sql .= "mrsbs_meeting_info.scheduledfor ";

	if($searchtext['comp'] == 'lt')
	  $sql .= "< ";
	elseif($searchtext['comp'] == 'eq')
	  $sql .= "= ";
	elseif($searchtext['comp'] == 'gt')
	  $sql .= "> ";

	$sql .= $dbc->DBTimeStamp($searchtext['val']);
      }
      
      if(strcasecmp($field, "scheduled")==0)
      {
	if($searchtext == 'Y')
	  $sql .= " mrsbs_meeting_info.scheduledfor IS NULL";
      }
    }
    
    $sql .= ")";
    
    $ret = $dbc->GetCol($sql, (count($sqlargs) > 0 ? $sqlargs : false));
  }

  return($ret);
}

function get_meeting_info($mtgid, $windowsort="pref")
{
  // Obtain all known information (except history) about $mtgid,
  // sorting meeting windows by $windowsort.

  // Returns: false on error, or an array of the form
  //  $ret['mtgid']: Meeting ID
  //  $ret['contactid']: Contact ID of this meeting's owner
  //  $ret['contactgivenname']: Given name of this meeting's owner
  //  $ret['contactsn']: Surname of this meeting's owner
  //  $ret['contactmail']: Mail of this meeting's owner
  //  $ret['hostid']: Contact ID of this meeting's host
  //  $ret['hostgivenname']: Given name of this meeting's host
  //  $ret['hostsn']: Surname of this meeting's host
  //  $ret['hostmail']: Email of this meeting's host
  //  $ret['summary']: Summary
  //  $ret['description']: Description
  //  $ret['duration']: Duration in minutes
  //  $ret['replyby']: Reply By
  //  $ret['replybymode']: Reply By Mode: [W]ait, Replies [R]eceived
  //  $ret['scheduledfor']: Scheduled For (valid only if status is 'S')
  //  $ret['location']: Location ID scheduled for (-1=none, -2=tbd)
  //  $ret['locationstatus']: [T]entative, [C]onfirmed, [N]one
  //  $ret['locinfo']: If Location is > 0, info as returned by get_location
  //  $ret['status']: [I]nvites sent, [S]cheduled, [B]uilding, [F]ailed,
  //                  lower=canceled
  //  $ret['invitees']['count']: # of invitees
  //  $ret['invitees'][#]['contactid']: Contact ID of invitee #
  //  $ret['invitees'][#]['mail']: Contact ID of invitee #
  //  $ret['invitees'][#]['givenname']: Given name of invitee #
  //  $ret['invitees'][#]['sn']: Surname of invitee #
  //  $ret['invitees'][#]['inviteid']: Invite ID of invitee #
  //  $ret['invitees'][#]['status']: Status of invitee #
  //  $ret['invitees'][#]['reply']: Reply for invitee #
  //  $ret['windows']['count']: # of windows
  //  $ret['windows'][#]['windowid']: ID of #
  //  $ret['windows'][#]['pref']: Preference of #
  //  $ret['windows'][#]['begin']: Begin time of #
  //  $ret['windows'][#]['end']: End time of #
  //  $ret['locations']['count']: # of potential locations
  //  $ret['locations'][#]['locationid']: Location ID of #
  //  $ret['locations'][#]['pref']: Preference of # (0=no, 1=ok, 2=preferred)
  //  $ret['locations'][#]['desc']: Description of #
  //  $ret['locations'][#]['capacity']: Capacity of #

  global $dbc;

  $ret = false;

  // General info

  $sql = $dbc->Prepare("SELECT * FROM mrsbs_meeting_info
                        WHERE mtgid=" . $dbc->Param('a'));
  
  $a = $dbc->GetRow($sql, array($mtgid));
  
  if(!$a)
    return(false);
  else
  {
    $ret = array();
    $ret['mtgid'] = $mtgid;
    $ret['contactid'] = $a['contactid'];
    $ret['hostid'] = $a['hostid'];
    $ret['summary'] = hsstr($a['summary']);
    $ret['description'] = hsstr($a['description']);
    $ret['duration'] = $a['duration'];
    $ret['replyby'] = $dbc->UnixTimeStamp($a['replyby']);
    $ret['replybymode'] = $a['replybymode'];
    $ret['scheduledfor'] = $dbc->UnixTimeStamp($a['scheduledfor']);
    $ret['location'] = $a['locationid'];
    $ret['locationstatus'] = $a['locationstatus'];
    $ret['status'] = $a['status'];

    // Get location info if set

    if($ret['location'] > 0)
      $ret['locinfo'] = get_location($ret['location']);
    else
      $ret['locinfo'] = array();
    
    // Lookup host and owner info

    $sql = $dbc->Prepare("SELECT givenname,sn,mail FROM mrsbs_contacts
                          WHERE contactid=" . $dbc->Param('a'));
    
    $a = $dbc->GetRow($sql, array($ret['contactid']));

    if($a)
    {
      $ret['contactgivenname'] = hsstr($a['givenname']);
      $ret['contactsn'] = hsstr($a['sn']);
      $ret['contactmail'] = hsstr($a['mail']);
    }
 
    $sql = $dbc->Prepare("SELECT givenname,sn,mail FROM mrsbs_contacts
                          WHERE contactid=" . $dbc->Param('a'));
    
    $a = $dbc->GetRow($sql, array($ret['hostid']));

    if($a)
    {
      $ret['hostgivenname'] = hsstr($a['givenname']);
      $ret['hostsn'] = hsstr($a['sn']);
      $ret['hostmail'] = hsstr($a['mail']);
    }
  }

  // Invitees

  $ret['invitees'] = array();
  
  $sql = $dbc->Prepare("SELECT mrsbs_contacts.givenname,
                               mrsbs_contacts.sn,
                               mrsbs_contacts.mail,
                               mrsbs_invitees.contactid,
                               mrsbs_invitees.inviteid,
                               mrsbs_invitees.status,
                               mrsbs_invitees.reply
                        FROM mrsbs_invitees
                        LEFT OUTER JOIN mrsbs_contacts
                        ON mrsbs_contacts.contactid=mrsbs_invitees.contactid
                        WHERE mtgid=" . $dbc->Param('a') . "
                        ORDER BY sn ASC");
  
  $records = $dbc->Execute($sql, array($mtgid));

  if($records)
  {
    $ret['invitees']['count'] = $records->RecordCount();
    $i = 0;
    
    while(!$records->EOF)
    {
      $ret['invitees'][$i] = array();
      $ret['invitees'][$i]['contactid'] = $records->fields['contactid'];
      $ret['invitees'][$i]['inviteid'] = $records->fields['inviteid'];
      $ret['invitees'][$i]['status'] = $records->fields['status'];
      $ret['invitees'][$i]['reply'] = $records->fields['reply'];
      $ret['invitees'][$i]['mail'] = hsstr($records->fields['mail']);
      $ret['invitees'][$i]['givenname'] = hsstr($records->fields['givenname']);
      $ret['invitees'][$i]['sn'] = hsstr($records->fields['sn']);
  
      $records->MoveNext();
      $i++;
    }
  }
  else
    $ret['invitees']['count'] = 0;

  // Windows
  
  $ret['windows'] = array();
  
  $sql = $dbc->Prepare("SELECT windowid, pref, begin, end
                        FROM mrsbs_windows
                        WHERE mtgid=" . $dbc->Param('a') . "
                        ORDER BY " . $windowsort . " ASC");
  
  $records = $dbc->Execute($sql, array($mtgid));

  if($records)
  {
    $ret['windows']['count'] = $records->RecordCount();
    $i = 0;
    
    while(!$records->EOF)
    {
      $ret['windows'][$i] = array();
      $ret['windows'][$i]['windowid'] = $records->fields['windowid'];
      $ret['windows'][$i]['pref'] = $records->fields['pref'];
      $ret['windows'][$i]['begin'] =
	$dbc->UnixTimeStamp($records->fields['begin']);
      $ret['windows'][$i]['end'] =
	$dbc->UnixTimeStamp($records->fields['end']);
      
      $records->MoveNext();
      $i++;
    }
  }
  else
    $ret['windows']['count'] = 0;
  
  // Potential locations
  
  $ret['locations'] = array();
  
  $sql = $dbc->Prepare("SELECT mrsbs_potential_locations.locationid,
                               mrsbs_potential_locations.pref,
                               mrsbs_locations.description,
                               mrsbs_locations.capacity
                        FROM mrsbs_potential_locations
                        INNER JOIN mrsbs_locations
                        ON mrsbs_potential_locations.locationid=mrsbs_locations.locationid
                        WHERE mtgid=" . $dbc->Param('a'));
  
  $records = $dbc->Execute($sql, array($mtgid));

  if($records)
  {
    $ret['locations']['count'] = $records->RecordCount();
    $i = 0;
    
    while(!$records->EOF)
    {
      $ret['locations'][$i] = array();
      $ret['locations'][$i]['locationid'] = $records->fields['locationid'];
      $ret['locations'][$i]['pref'] = $records->fields['pref'];
      $ret['locations'][$i]['desc'] = hsstr($records->fields['description']);
      $ret['locations'][$i]['capacity'] = $records->fields['capacity'];
      
      $records->MoveNext();
      $i++;
    }
  }
  else
    $ret['locations']['count'] = 0;
  
  return($ret);
}

function get_meeting_owner($mtgid)
{
  // Obtain the contactid who owns $mtgid.

  // Returns: The contactid, or false on error.

  global $dbc;

  $sql = $dbc->Prepare("SELECT contactid FROM mrsbs_meeting_info
                       WHERE mtgid=" . $dbc->Param('a'));
  
  return($dbc->GetOne($sql, array($mtgid)));
}

function get_meetings($contactid = -1, $searchtext = "", $searchfields = false)
{
  // Obtain information about all known meetings, or if $contactid is
  // specified those meetings owned or hosted by $contactid.  If
  // $searchtext is specified, only return those meetings containing
  // $searchtext in the summary or description.  If $searchfields is specified,
  // is is an array using the field to search as a key and the search text as
  // the key's value.  Valid keys are "summary", "description", "owner",
  // "host", "invitees", "location", "mtgid". "status", "duration",
  // "replyby", "scheduledfor", "scheduled"
  
  // Returns: false on error, or an array of the form
  //  $ret['count']: Number of matching meetings
  //  $ret[#]['mtgid']: Meeting ID of #
  //  $ret[#]['contactid']: Contact ID of this meeting's owner
  //  $ret[#]['contactgivenname']: Given name of this meeting's owner
  //  $ret[#]['contactsn']: Surname of this meeting's owner
  //  $ret[#]['contactmail']: Mail of this meeting's owner
  //  $ret[#]['hostid']: Contact ID of this meeting's host
  //  $ret[#]['hostgivenname']: Given name of this meeting's host
  //  $ret[#]['hostsn']: Surname of this meeting's host
  //  $ret[#]['hostmail']: Email of this meeting's host
  //  $ret[#]['summary']: Summary
  //  $ret[#]['description']: Description
  //  $ret[#]['duration']: Duration in minutes
  //  $ret[#]['replyby']: Reply By
  //  $ret[#]['scheduledfor']: Scheduled For (valid only if status is 'S')
  //  $ret[#]['location']: Location ID scheduled for (-1=none, -2=tbd)
  //  $ret[#]['locationstatus']: [T]entative, [C]onfirmed, [N]one
  //  $ret[#]['status']: [I]nvites sent, [S]cheduled, [B]uilding, [F]ailed
  //                     lower=canceled

  global $dbc;
  $ret = false;

  $cmtgids = array();
  $smtgids = array();

  if($searchtext != "" || $searchfields)
  {
    // Retrieve meetings based on search tokens
    
    // If $contactid is provided, we only return meetings viewable by that
    // contactid.  (We AND the results.)
    
    if($contactid > -1)
    {
      $cmtgids = get_meeting_ids_contactid_own_host($contactid);
    }

    // We only perform a simple ($searchtext) or advanced ($searchfields),
    // not both.  If both are provided, we'll do the advanced.

    if($searchtext && !$searchfields)
    {
      // Tokenize $searchtext and perform AND, then OR

      $tokens = preg_split("/[\s,]+/", $searchtext);

      foreach($tokens as $token)
      {
	$smtgids[] = get_meeting_ids_string_match($token);
      }

      // Find meetings that matched all tokens.  Since array_intersect
      // can't be passed an array of arrays, we just intersect one at
      // a time.

      $andarray = $smtgids[0];

      for($i = 1;$i < count($smtgids);$i++)
      {
	$andarray = array_intersect($andarray, $smtgids[$i]);
      }

      // For OR, we work similarly, but merge and then unique the results.

      $orarray = $smtgids[0];

      for($i = 1;$i < count($smtgids);$i++)
      {
	$orarray = array_merge($orarray, $smtgids[$i]);
      }

      $orarray = array_unique($orarray);

      // Now combine the AND and OR results and unique them.  The AND results
      // should come first.

      $combined = array_unique(array_merge($andarray, $orarray));

      // If $contactid is set we only return those meetings owned or hosted
      // by $contactid (a permission check)

      if($contactid > -1)
      {
	$smtgids = array_intersect($combined, $cmtgids);
      }
      else
      {
	$smtgids = $combined;
      }
    }

    // Advanced search
    
    if($searchfields)
    {
      // The data structure here could be a bit confusing, so for
      // simplicity the first index into the array is the searchfield
      // key (ie: text), and the search tokens for each field are numerically
      // indexed within the search field.
      
      $search_field_results = array();

      foreach(array_keys($searchfields) as $k)
      {
	if($searchfields[$k] != "")
	{
	  // Split each search field into a list of tokens and obtain the
	  // results for each token.  DON'T do this for "special" fields
	  // where $searchfield is actually an array -- just pass the array.
	  
	  if($k == "duration" || $k == "replyby" || $k == "start")
	  {
	    $search_field_results[$k][] =
	      get_meeting_ids_string_match($searchfields[$k], $k);
	  }
	  else
	  {
	    $tokens = preg_split("/[\s,]+/", $searchfields[$k]);

	    foreach($tokens as $token)
	    {
	      $search_field_results[$k][] =
		get_meeting_ids_string_match($token, $k);
	    }
	  }
	}
      }
      
      // AND the results of each field token (ie: the numbers)
      // together, and AND those results together.  We are working
      // with an array of field results, each of which is an array of
      // token results.

      $andandarray = array();
      $first = true;
      
      foreach(array_keys($searchfields) as $k)
      {
	if($searchfields[$k] != "")
	{
	  // Calculate the AND of the tokens
	
	  $subandarray = $search_field_results[$k][0];
	  
	  for($i = 1;$i < count($search_field_results[$k]);$i++)
	  {
	    $subandarray = array_intersect($subandarray,
					   $search_field_results[$k][$i]);
	  }

	  // AND the token results with the field results
	  
	  if($first)
	  {
	    $andandarray = $subandarray;
	    $first = false;
	  }
	  else
	  {
	    $andandarray = array_intersect($andandarray, $subandarray);
	  }
	}
      }
      
      // OR the results of each field token together, and AND those results
      // together.

      $andorarray = array();
      $first = true;
      
      foreach(array_keys($searchfields) as $k)
      {
	if($searchfields[$k] != "")
	{
	  // Calculate the OR of the tokens
	
	  $suborarray = $search_field_results[$k][0];

	  for($i = 1;$i < count($search_field_results[$k]);$i++)
	  {
	    $suborarray = array_merge($suborarray,
				      $search_field_results[$k][$i]);
	  }

	  // Toss any duplicates
	
	  $suborarray = array_unique($suborarray);

	  // AND the token results with the field results
	
	  if($first)
	  {
	    $andorarray = $suborarray;
	    $first = false;
	  }
	  else
	  {
	    $andorarray = array_intersect($andorarray, $suborarray);
	  }
	}
      }

      // OR the results of each field token together, and OR those results
      // together.
      
      $ororarray = array();
      
      foreach(array_keys($searchfields) as $k)
      {
	if($searchfields[$k] != "")
	{
	  // Calculate the OR of the tokens
	
	  $suborarray = $search_field_results[$k][0];

	  for($i = 1;$i < count($search_field_results[$k]);$i++)
	  {
	    $suborarray = array_merge($suborarray,
				      $search_field_results[$k][$i]);
	  }

	  // OR the token results with the field results.  We have a slight
	  // order difference from above since OR/OR is fully transitive.

	  $ororarray = array_merge($ororarray, $suborarray);
	  
	  // Toss any duplicates
	
	  $ororarray = array_unique($ororarray);
	}
      }
      
      // Now combine the results and unique them.  The AND results
      // should come first.

      $combined = array_unique(array_merge($andandarray,
					   $andorarray,
					   $ororarray));

      // If $contactid is set we only return those meetings owned or hosted
      // by $contactid (a permission check)

      if($contactid > -1)
      {
	$smtgids = array_intersect($combined, $cmtgids);
      }
      else
      {
	$smtgids = $combined;
      }
    }
  }
  else
  {
    // Retrieve all meetings (or those relevant for $contactid).
    
    if($contactid > -1)
      $smtgids = get_meeting_ids_contactid_own_host($contactid);
    else
      $smtgids = get_meeting_ids();
  }

  // If we found anything, pull out the appropriate data for those meetings.

  if(count($smtgids) > 0)
  {
    // We do not include an ORDER BY clause since everything should
    // instead use render_list, which allows order by criteria.  This
    // allows us to retain search rank/"relevance" order.
    
    $sql = $dbc->Prepare("SELECT mrsbs_meeting_info.*,
                                 mrsbs_contacts.givenname AS contactgivenname,
                                 mrsbs_contacts.sn AS contactsn,
                                 mrsbs_contacts.mail AS contactmail,
                                 m2.givenname AS hostgivenname,
                                 m2.sn AS hostsn,
                                 m2.mail AS hostmail
                          FROM mrsbs_meeting_info
                          LEFT OUTER JOIN mrsbs_contacts
                          ON mrsbs_contacts.contactid=mrsbs_meeting_info.contactid
                          LEFT OUTER JOIN mrsbs_contacts AS m2
                          ON m2.contactid=mrsbs_meeting_info.hostid
                          WHERE mrsbs_meeting_info.mtgid IN (
                          " . implode(",", $smtgids) . "
                          )");

    $records = $dbc->Execute($sql, (count($sqlargs) > 0 ? $sqlargs : false));
			 
    if($records)
    {
      $ret['count'] = $records->RecordCount();
      $i = 0;
	
      while(!$records->EOF)
      {
	$ret[$i] = array();
	$ret[$i]['mtgid'] = $records->fields['mtgid'];
	$ret[$i]['contactid'] = $records->fields['contactid'];
	$ret[$i]['contactgivenname'] =
	  hsstr($records->fields['contactgivenname']);
	$ret[$i]['contactsn'] = hsstr($records->fields['contactsn']);
	$ret[$i]['contactmail'] = hsstr($records->fields['contactmail']);
	$ret[$i]['hostid'] = $records->fields['hostid'];
	$ret[$i]['hostgivenname'] = hsstr($records->fields['hostgivenname']);
	$ret[$i]['hostsn'] = hsstr($records->fields['hostsn']);
	$ret[$i]['hostmail'] = hsstr($records->fields['hostmail']);
	$ret[$i]['summary'] = hsstr($records->fields['summary']);
	$ret[$i]['description'] = hsstr($records->fields['description']);
	$ret[$i]['duration'] = $records->fields['duration'];
	$ret[$i]['replyby'] =
	  $dbc->UnixTimeStamp($records->fields['replyby']);
	$ret[$i]['scheduledfor'] =
	  $dbc->UnixTimeStamp($records->fields['scheduledfor']);
	$ret[$i]['locationid'] = $records->fields['locationid'];
	$ret[$i]['locationstatus'] = $records->fields['locationstatus'];
	$ret[$i]['status'] = $records->fields['status'];
	
	$records->MoveNext();
	$i++;
      }
    }

    return($ret);
  }
}

function get_permitted_locations($contactid, $groups)
{
  // Obtain the locations that may be booked by the user $contactid or a member
  // of any of the groups in the array $groups.

  // Returns: false on error, or an array of the form
  //  $ret['count']: The number of locations
  //  $ret[#]['locationid']: ID
  //  $ret[#]['description']: Description
  //  $ret[#]['capacity']: Capacity
  //  $ret[#]['contactid']: Contact ID who owns location
  //  $ret[#]['givenname']: Given name of this meeting's owner
  //  $ret[#]['sn']: Surname of this meeting's owner
  //  $ret[#]['mail']: Email of this meeting's owner
  //  $ret[#]['system']: [I]nternal or [M]RBS

  global $dbc;

  $ret = false;
  
  $sql = 'SELECT mrsbs_locations.*,
                 mrsbs_contacts.givenname,
                 mrsbs_contacts.sn,
                 mrsbs_contacts.mail
          FROM mrsbs_locations
          LEFT OUTER JOIN mrsbs_contacts
          ON mrsbs_contacts.contactid=mrsbs_locations.contactid
          WHERE locationid
          IN (SELECT DISTINCT resourceid
              FROM mrsbs_acls
              WHERE resourcetype="L" AND perm=1 AND (';

  if(isset($groups) && count($groups) > 0)
  {
    // Quote all groups before joining them into the query string
    $sgroups = array_map("sstr", $groups);
    
    $sql .= '(acltype="G" AND who IN (' . implode(',', $sgroups) . ')) OR ';
  }
  
  $sql .= '(acltype="U" AND who=' . sstr($contactid) . ') OR acltype="A"))
          ORDER BY mrsbs_locations.description ASC
   ';
  
  $records = $dbc->Execute($sql);

  if($records)
  {
    $ret = array();
    $ret['count'] = $records->RecordCount();
    $i = 0;
    
    while(!$records->EOF)
    {
      $ret[$i] = array();
      $ret[$i]['locationid'] = $records->fields['locationid'];
      $ret[$i]['description'] = hsstr($records->fields['description']);
      $ret[$i]['capacity'] = $records->fields['capacity'];
      $ret[$i]['contactid'] = $records->fields['contactid'];
      $ret[$i]['mail'] = hsstr($records->fields['mail']);
      $ret[$i]['givenname'] = hsstr($records->fields['givenname']);
      $ret[$i]['sn'] = hsstr($records->fields['sn']);
      $ret[$i]['system'] = $records->fields['system'];
  
      $records->MoveNext();
      $i++;
    }
  }
  
  return($ret);
}

function get_recent_invitees($contactid, $inviteesort="sn")
{
  // Obtain the recent invitees for $contactid, sorting invitees by
  // $inviteesort.

  // Returns: false on error, or an array of the form
  //  $ret['count']: The number of invitees
  //  $ret[#]['contactid']: Contact ID of invitee
  //  $ret[#]['givenname']: Given name of this invitee
  //  $ret[#]['sn']: Surname of this invitee
  //  $ret[#]['mail']: Email of this invitee
  //  $ret[#]['lastinvite']: Time this invitee was last invited

  global $dbc;
  $ret = false;

  $sql = 'SELECT mrsbs_recent_invitees.*,
                 mrsbs_contacts.givenname,
                 mrsbs_contacts.sn,
                 mrsbs_contacts.mail
          FROM mrsbs_recent_invitees
          LEFT OUTER JOIN mrsbs_contacts
          ON mrsbs_contacts.contactid=mrsbs_recent_invitees.inviteeid
          WHERE mrsbs_recent_invitees.contactid=1002
          ORDER BY ' . $inviteesort . ' ASC';

  $records = $dbc->Execute($sql);
  
  if($records)
  {
    $ret = array();
    $ret['count'] = $records->RecordCount();
    $i = 0;
    
    while(!$records->EOF)
    {
      $ret[$i] = array();
      $ret[$i]['contactid'] = $records->fields['inviteeid'];
      $ret[$i]['givenname'] = hsstr($records->fields['givenname']);
      $ret[$i]['sn'] = hsstr($records->fields['sn']);
      $ret[$i]['mail'] = hsstr($records->fields['mail']);
      $ret[$i]['lastinvite'] =
	$dbc->UnixTimeStamp($records->fields['lastinvite']);
  
      $records->MoveNext();
      $i++;
    }
  }
  
  return($ret);
}

function hstr($s)
{
  // Make $s safe for substituting into an HTML document.

  // Returns: A sanitized string.

  return(htmlspecialchars($s));
}

function hsstr($s)
{
  // Make $s safe for substituting into an HTML document, and strip
  // slashes that were inserted by sstr().

  // Returns: A sanitized string.

  return(htmlspecialchars(stripslashes($s)));
}

function lookup_user_by_email($email)
{
  // Look up a user by email address.

  // Returns: false on error, or an array of the form
  //  $ret['contactid']: Numerical identifier
  //  $ret['mail']: Email address
  //  $ret['givenname']: Given Name (if directory lookups are enabled)
  //  $ret['sn']: Surname (if directory lookups are enabled)

  global $dbc;
  global $config;

  $ret = false;
  
  // First, see if $email is in the database

  $sql = $dbc->Prepare("SELECT contactid, givenname, sn FROM mrsbs_contacts
                        WHERE mail=" . $dbc->Param('a'));

  $records = $dbc->Execute($sql, array(maybe_obscure_email($email)));
  
  if($records && $records->RecordCount() == 1)
  {
    $ret = array();
    $ret['contactid'] = $records->fields['contactid'];
    $ret['mail'] = hsstr($email);
    $ret['givenname'] = hsstr($records->fields['givenname']);
    $ret['sn'] = hsstr($records->fields['sn']);
  }
  else
  {
    // If not found, lookup givenname and sn (if ldap is enabled)
    // and insert into the database.  There must be exactly one match,
    // otherwise return false.

    if($config['ldaphost'] != "")
    {
      $dirq = directory_query($email);
    }
    else
    {
      // Fake out a reply consisting of just this email address if LDAP
      // is not enabled.

      $dirq = array();
      $dirq['count'] = 1;
      $dirq[0] = array();
      $dirq[0]['givenname'] = array();
      $dirq[0]['givenname'][0] = "";
      $dirq[0]['sn'] = array();
      $dirq[0]['sn'][0] = "";
    }

    if($dirq && $dirq['count'] == 1)
    {
      $contactid = $dbc->GenID('mrsbs_contactid_seq', 1001);
      
      $sql = $dbc->Prepare('INSERT INTO mrsbs_contacts
                            (contactid, mail, givenname, sn)
                            VALUES(' . $dbc->Param('a') . ',' .
			               $dbc->Param('b') . ',' .
			               $dbc->Param('c') . ',' .
			               $dbc->Param('d') . ')');
    
      $records = $dbc->Execute($sql, array($contactid,
					   maybe_obscure_email($email),
					   $dirq[0]['givenname'][0],
					   $dirq[0]['sn'][0]));
      
      if($records)
      {
	$ret = array();
	$ret['contactid'] = $contactid;
	$ret['mail'] = hstr($records->fields['mail']);
	$ret['givenname'] = hstr($records->fields['givenname']);
	$ret['sn'] = hstr($records->fields['sn']);
      }
      else
	print $dbc->ErrorMsg() . "<P>";
    }
  }

  return($ret);
}

function maybe_obscure_email($mail)
{
  // If in demo mode, return an obscured version of $mail.

  // returns: A string containing $mail, possibly obscured.

  global $config;

  if($config['demomode'])
  {
    $x = preg_split('/@/', $mail, 2);

    return($x[0] . '@' . crypt($x[1], $x[0]));
  }
  else
    return($mail);
}

function mexit($msg, $err)
{
  // Exit by generating a page with $msg, which is an error if $err is true.

  // Returns: Does not return.

  global $pgtitle;
  global $tx;
  global $auth_logout_provided;
  
  if($err)
    $rvar_error = $msg;
  else
    $rvar_result = $msg;
  
  include "result.php";
  
  exit();
}

function random_identifier()
{
  // Generate a random identifier.

  // Returns: A random identifier.

  // mt_getrandmax is 2g.  Call it twice for a ~20 character string.
  
  return(mt_rand() . mt_rand());
}

function record_history($mtgid, $contactid, $action, $desc)
{
  // Write a history record for meeting $mtgid, with operation triggered by
  // $contactid, encoded as $action, and described in $desc.

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $sql = 'INSERT INTO mrsbs_history
          (mtgid, contactid, rtime, action, description)
          VALUES (' . sstr($mtgid) . ',' .
                      sstr($contactid) . ',' .
		      $dbc->DBTimeStamp(time()) . ',' .
                      sstr($action) . ',' .
                      sstr($desc) . ')';

  return($dbc->Execute($sql));
}

function reinstate_meeting($mtgid)
{
  // Reinstate the meeting $mtgid, sending notifications if appropriate.

  // Returns: true if fully successful, false otherwise.

  global $tx;
  
  $ret = false;

  if($mtgid)
  {
    $oldstat = update_meeting_status($mtgid, "x");

    if($oldstat)
    {  
      switch($oldstat)
      {
      case 'i':
	// Re-send invites
	send_invitations($mtgid, $tx['in.uncanceled']);
	break;
      case 's':
	// Re-send notifications
	send_notifications($mtgid, $tx['in.uncanceled']);
	break;
      }
      
      $ret = true;
    }
    // else can't reinstate an active meeting
  }
  
  return($ret);
}

function render_contacts_select($cts, $allownone, $selected=0)
{
  // Output <OPTION> values keyed on contactid for $cts, an array of
  // the form returned by get_contacts().  If $allownone is true, an
  // option to select nobody is provided.  Set the selected option to
  // the entry with contactid $selected.

  // Returns: Nothing.

  if($allownone)
    print '<OPTION VALUE="-1">-</OPTION>' . "\n";
  
  for($i = 0;$i < $cts['count'];$i++)
    print '<OPTION VALUE="' . $cts[$i]['contactid'] . '"' .
      ($cts[$i]['contactid'] == $selected ? " SELECTED" : "") . '>' .
      render_name("full", $cts[$i]['givenname'], $cts[$i]['sn'],
		  $cts[$i]['mail']) .
      "</OPTION>\n";
}

function render_day($d, $m, $y)
{
  // Render $d, $m, and $y into a human readable string.

  // Returns: A string containing the day.

  global $tx;
  
  return($d . ' ' . $tx['mon'][$m-1] . ' ' . $y);
}

function render_list($l, $nonetext, $sortkey=0, $sortorder="asc",
		     $showpage=-1, $perpage=-1, $label=false)
{
  // Render a sortable list in a table, based on the contents of $l, which
  // is an array of the form
  //  $l['column']['count']: Number of headings (columns)
  //  $l['column'][#]['title']: Title of column #
  //  $l['column'][#]['sortable']: Column # is [N]ot sortable,
  //   [A]scending sort default, [D]escending sort default
  //  $l['row']['count']: Number of rows
  //  $l['row'][#][##]['value']: Value of row #, column ##
  //  $l['row'][#][##]['sortval']: Sort value of row #, column ##
  // Sort based on the column $sortkey in $sortorder, which "asc" or "desc".
  // Start rendering from $showpage, with $perpage entries per page
  // (-1 gets the default behavior for either).
  // If $label is true, label each row with a row count.
  // If there are no items in the list, render $nonetext instead.
  // This function will generate URIs with the following arguments appended
  // to the query string for use in sorting and pagination:
  //  sortkey, sortorder, showpage, perpage

  // Returns: Nothing.

  global $config;
  
  if($l && $l['column']['count'] > 0 && $l['row']['count'] > 0
     && ($sortkey > -1) && ($sortkey < $l['column']['count']))
  {
    // First, build a sorted array.

    $skeys = array();
    $svals = array();

    // Load in the sortval and the row number it points to

    for($i = 0;$i < $l['row']['count'];$i++)
    {
      array_push($skeys, $l['row'][$i][$sortkey]['sortval']);
      array_push($svals, $i);
    }
    
    // Now sort the keys, if the column is sortable

    if($l['column'][$sortkey]['sortable'] != 'N')
    {
      if($sortorder == "asc")
	array_multisort($skeys, SORT_ASC, $svals);
      else
	array_multisort($skeys, SORT_DESC, $svals);
    }
    
    // Finally, render the list.  First the header.
    
    print '
     <TABLE>
     <TR>
    ';

    if($label)
      print '
      <TD CLASS="formheader">
        ' . $j . '
      </TD>
        ';
      
    for($i = 0;$i < $l['column']['count'];$i++)
    {
      $closetag = 0;
            
      print '
      <TD CLASS="formheader">
        ' . $l['column'][$i]['title'];

      if($l['column'][$i]['sortable'] != 'N')
      {
	$args = array("sortkey", "sortorder");
	$valsa = array($i, "asc");
	$valsb = array($i, "desc");

	if($i == $sortkey && $sortorder == 'asc')
	  print '&uarr;';
	else
	  print '<A HREF="' . build_uri_args(true, $args, $valsa) .
	    '">&uarr;</A>';
	
	if($i == $sortkey && $sortorder == 'desc')
	  print '&darr;';
	else
	  print '<A HREF="' . build_uri_args(true, $args, $valsb) .
	    '">&darr;</A>';
      }

      print '
       </TD>
      ';
    }

    print '
     </TR>
    ';

    // And now the data, render in order of the sorted array $skeys,
    // using $svals to index into the original data.
    
    // Paginate appropriately.  All page counts start at 1, since they
    // are externally visible.  All item counts start at 0, since they
    // are references to internal arrays.

    // Number of items per page
    if($perpage < 1)
      $l_perpage = $config['listperpage'];
    else
      $l_perpage = $perpage;

    // Total number of items
    $l_itemcount = count($skeys);
    
    // Maximum number of pages
    $l_maxpage = floor(($l_itemcount - 1) / $l_perpage) + 1;

    // The page to render
    if($showpage < 1)
      $l_showpage = 1;
    elseif($showpage > $l_maxpage)
      $l_showpage = $l_maxpage;
    else
      $l_showpage = $showpage;
       
    // First item to render
    $l_firstitem = ($l_showpage - 1) * $l_perpage;
       
    // Last item to render
    $l_lastitem = ($l_showpage * $l_perpage) - 1;
    if($l_lastitem > $l_itemcount)
      $l_lastitem = $l_itemcount - 1;

    for($j = $l_firstitem;$j <= $l_lastitem;$j++)
    {
      print '
     <TR>
      ';

      if($label)
	print '
      <TD CLASS="formheader">
        ' . ($j+1) . '
      </TD>
        ';
      
      for($i = 0;$i < $l['column']['count'];$i++)
      {
	print '
      <TD CLASS="formfield' . (($j % 2 == 1) ? '1' : '') . '">
        ' . $l['row'][$svals[$j]][$i]['value'] . '
      </TD>
        ';
      }
      
      print '
     </TR>
      ';
    }

    print '
     <TR>
      <TD CLASS="pagejumpbar" COLSPAN=' .
      ($label ? ($l['column']['count'] + 1) : $l['column']['count']) . '>
    ';

    if($l_maxpage > 1)
    {
      // Generate "jump to page" links
    
      // We don't want the default perpage to show up in bookmarks
      
      if($l_perpage == $config['listperpage'])
      {
	$args = array("showpage");
	$x_vals = array();
      }
      else
      {
	$args = array("perpage", "showpage");
	$x_vals = array($l_perpage);
      }
      
      if($l_showpage > 1)
      {
	$vals = $x_vals;
	$vals[] = 1;
      
	print '<A HREF="' . build_uri_args(true, $args, $vals) .
	  '">&lt;&lt;</A> ';
      
	$vals = $x_vals;
	$vals[] = $l_showpage - 1;
	
	print '<A HREF="' . build_uri_args(true, $args, $vals) .
	  '">&lt;</A> ';
      }
      else
	print '&lt;&lt; &lt; ';
      
      for($i = 1;$i <= $l_maxpage;$i++)
      {
	$vals = $x_vals;
	$vals[] = $i;
	
	// Render a link if not the current page
	
	if($i == $l_showpage)
	  print $i . ' ';
	else
	  print '<A HREF="' . build_uri_args(true, $args, $vals) . '">' .
	    $i . '</A> ';
      }
      
      if($l_showpage < $l_maxpage)
      {
	$vals = $x_vals;
	$vals[] = $l_showpage + 1;
	
	print '<A HREF="' . build_uri_args(true, $args, $vals) .
	  '">&gt;</A> ';
	
	$vals = $x_vals;
	$vals[] = $l_maxpage;
	
	print '<A HREF="' . build_uri_args(true, $args, $vals) .
	  '">&gt;&gt;</A> ';
      }
      else
	print '&gt; &gt;&gt;';
      
      $args = array("perpage", "showpage");
      $vals = array($l_itemcount, 1);
      
      print ' [' . ($l_firstitem + 1) . '-' . ($l_lastitem + 1) . '/' .
	'<A HREF="' . build_uri_args(true, $args, $vals) . '">' .
	$l_itemcount . '</A>]';
    }
    else
      print ' [' . ($l_firstitem + 1) . '-' . ($l_lastitem + 1) . '/' .
	$l_itemcount . ']';
    
    print '
      </TD>
     </TR>
     </TABLE>
    ';
  }
  else
    print $nonetext;
}

function render_location($locid, $locinfo, $locstatus)
{
  // Render information about the location $locid, which may have
  // additional information described in $locinfo (as returned by
  // get_location_info) and $locstatus.

  // Returns: A string containing the location information.

  global $tx;

  switch($locid)
  {
  case -2:
    $r = $tx['sd.locs.tbd'];
    break;
  case -1:
    $r = $tx['sd.locs.no'];
    break;
  case 0:
    break;
  default:
    $r = $locinfo['description'];
    if($locstatus == "T")
      $r .= " (" . $tx['sd.tentative'] . ")";
    if($locstatus == "C")
      $r .= " (" . $tx['sd.confirmed'] . ")";
    break;
  }

  return($r);
}

function render_name($format, $givenname, $sn, $mail)
{
  // Render the name for $givenname, $sn, and $mail.  Format is
  // "compact" to show only the best of name/mail, "full" to show
  // as much as possible, "mail" to generate a address suitable for
  // sending mail to, "mailfrom" to generate a address suitable for
  // sending mail from.

  // Returns: A string holding the name.

  global $tx;
  global $config;
  
  $n = "";
  $g = $givenname;
  $s = $sn;
  $m = $mail;

  if($format == "mail" && $config['debugemail'] != "")
    $m = $config['debugemail'];

  if($format == "mail" || $format == "mailfrom")
  {
    // Escape some characters

    // Quote quotes in name or mail

    $g = preg_replace('/"/', '/\\"/', $g);
    $s = preg_replace('/"/', '/\\"/', $s);
    $m = preg_replace('/"/', '/\\"/', $m);

    // Quote <> in mail

    if(strstr($m, "<") || strstr($m, ">"))
    {
      $m = '"' . $m . '"';
    }
  }
  
  if($format == "compact")
  {
    if($g != "" || $s != "")
    {
      if($g != "")
      {
	$n = $g;

	if($s != "")
	  $n .= " ";
      }

      if($s != "")
        $n .= $s;
    }
    else
      $n = $m;
  }
  else
  {
    if($format == "mail" || $format == "mailfrom")
      $n = '"';
    
    if($g != "")
    {
      $n .= $g;
      
      if($s != "")
	$n .= " ";
    }

    if($s != "")
      $n .= $s;
    
    if($format == "mailfrom")
      $n .= ' ' . $tx['progname.mail'];

    if($format == "mail" || $format == "mailfrom")
      $n .= '"';

    if($m != "")
    {
      if($n != "")
	$n .= " ";

      if($format == "mail" || $format == "mailfrom")
	$n .= "<";
      else
	$n .= "[";
      
      $n .= $m;

      if($format == "mail" || $format == "mailfrom")
	$n .= ">";
      else
	$n .= "]";
    }
  }
  
  return($n);
}

function render_time($h, $m)
{
  // Render $h and $m into a human readable string.

  // Returns: A string containing the time $h:$m.

  global $config;
  global $tx;
  
  if($config['ampm'])
  {
    $am = 1;
    $hx = $h;
    
    if($h >= 12)
      $am = 0;

    if($h > 12)
      $hx = $h - 12;
    elseif($h == 0)
      $hx = 12;

    return(sprintf("%d:%02d %s", $hx, $m, $am ? $tx['am'] : $tx['pm']));
  }
  else
    return(sprintf("%02d:%02d", $h, $m));
}

function rfc2445_text($s)
{
  // Generate an RFC 2445 Text object compliant string from $s.

  // Returns: An RFC 2445 compliant string.

  // Well, actually, all we do right now is get rid of \r.  Bennu
  // will handle the rest of the hard work.
  $r = preg_replace('/\r/', '', $s);

  return($r);
}

function schedule_event($mtgid)
{
  // Schedule $mtgid.  Notifications are NOT sent.

  // Returns: 1 if the event was (re)scheduled, 2 if the event was
  // (re)located, 3 if both, or 0 if the meeting was previously
  // scheduled but nothing needed to be changed.

  global $tx;
  
  $ret = 0;
  
  $mtg = get_meeting_info($mtgid);
    
  // Before we start looking at windows, sort the locations.
  // Location preference doesn't change according to window.

  $mtgloc = "";
  $mtglocid = $mtg['location'];
  $mtglocconfirm = "C";

  switch($mtg['location'])
  {
  case '-2':
    $mtgloc = $tx['sd.locs.tbd'];
    break;
  case '-1':
    $mtgloc = $tx['sd.locs.no'];
    break;
  case '0':
    // We need to calculate the location
    $locs = sort_meeting_locations($mtg);
    $mtglocconfirm = "T";
    break;
  default:
    // A location has already been assigned
    $l = get_location($mtg['location']);
    if($l)
      $mtgloc = $l['description'];
    else
      $mtgloc = $tx['op.notfound'];
    break;
  }
  
  // It's possible we won't find a matching window
  $matched = false;
  
  $windows = sort_meeting_windows($mtg);

  for($i = 0;$i < count($windows);$i++)
  {
    if($mtglocid == 0)
    {
      // We need to find a location.  If we were integrated with
      // MRBS, we would iterate through $locs in order until we
      // found an available location.  If none were found, we'd move
      // on to the next window.  We would set $mtglocconfirm to "C"
      // once we reserved a location.
      
      // Since we're not, we just pick the first location and the
      // first window.

      if(count($locs) > 0)
      {
	$mtgloc = $locs[0]['desc'];
	$mtglocid = $locs[0]['locationid'];
	$matched = $windows[$i];
      }
    }
    else
    {
      // No need to find a room.  Select the first meeting window.

      $matched = $windows[$i];
    }
      
    if($matched)
    {
      // Update mrsbs_meeting_info

      if(($mtglocid != $mtg['location'])
	 || ($mtglocconfirm != $mtg['locationstatus']))
      {
	update_meeting_location($mtgid, $mtglocid, $mtglocconfirm);
	$ret += 2;
      }

      if($matched['begin'] != $mtg['scheduledfor'])
      {
	update_meeting_time($mtgid, $matched['begin']);
	$ret += 1;
      }

      if($mtg['status'] != "S")
	update_meeting_status($mtgid, "S");

      if($ret)
	record_history($mtgid,
		       (isset($_SESSION['contactid']) ?
			$_SESSION['contactid'] :
			"cron"),
		       "CALC",
		       $tx['hi.calc']);
      
      break;
    }
  }

  return($ret);
}

function schedule_ready_events()
{
  // Determine which events are ready to be scheduled and schedule them.
  // This will also send calendar messages.

  // Returns: true if fully successful, false otherwise.

  global $dbc, $config;

  if($config['debug'])
    print "Meetings ready to be scheduled because\n";
  
  // An event is ready if status='I' (invites sent) and one of the following
  // is true:
  //  1. the replyby time has elapsed
  //  2. all required and optional invitees have replied
  //     AND replybymode is R
  //  3. there is only one window, and it is of length duration
  //     AND replybymode is not W

  $sql = "SELECT mtgid
          FROM mrsbs_meeting_info
          WHERE status='I'
          AND replyby < " . $dbc->OffsetDate(0, $dbc->sysTimeStamp);
  
  $mtgs1 = $dbc->GetCol($sql);

  if($config['debug'])
    print "1. Reply by time has passed: " . implode(",", $mtgs1) . "\n";
  
  $sql = "SELECT DISTINCT mtgid
          FROM mrsbs_invitees AS t
          WHERE (SELECT COUNT(*)
                 FROM mrsbs_invitees AS a
                 WHERE a.status!='N' AND a.reply!='N' AND t.mtgid=a.mtgid
                 GROUP BY mtgid)
                =
                (SELECT COUNT(*)
                 FROM mrsbs_invitees AS b
                 WHERE b.status!='N' AND t.mtgid=b.mtgid
                 GROUP BY mtgid)
          AND mtgid IN
              (SELECT mtgid FROM mrsbs_meeting_info
               WHERE replybymode='R' AND status='I')";
		 
  $mtgs2 = $dbc->GetCol($sql);

  if($config['debug'])
    print "2. All invitees have replied: " . implode(",", $mtgs2) . "\n";
  
  // It doesn't look like we can use SQL to compare end-begin since mysql
  // returns (eg) 2006-04-27 17:00:00 - 2006-04-27 14:30:00 = 27000.000000.
  // Even if that's portable, it's not straightforward to compare against
  // duration.
  $sql = "SELECT mtgid
          FROM mrsbs_meeting_info
          WHERE status='I'
          AND replybymode!='W'
          AND (SELECT COUNT(*) FROM mrsbs_windows
               WHERE mrsbs_meeting_info.mtgid=mrsbs_windows.mtgid) = 1";

  $mtgs3 = $dbc->GetCol($sql);
  $mtgs3ok = array();

  // $mtgs3 has all one-window meetings.  Check them for duration.

  foreach($mtgs3 as $m)
  {
    $minfo = get_meeting_info($m);

    if($minfo &&
       ($minfo['windows'][0]['end'] - $minfo['windows'][0]['begin']
	== ($minfo['duration'] * 60)))
      $mtgs3ok[] = $m;
  }

  if($config['debug'])
    print "3. There is one window of length duration: " .
      implode(",", $mtgs3ok) . "\n";

  // Note a meeting may be returned by more than one of these
  // criteria.  If we process each group and update status to S, that
  // wouldn't be a problem, but instead we'll just calculate the unique
  // list.

  $todo = array_unique(array_merge($mtgs1, $mtgs2, $mtgs3ok));

  foreach($todo as $t)
  {
    if(schedule_event($t))
      send_notifications($t);
    else
    {
      update_meeting_status($t, "F");
      send_failed_to_schedule($t);
    }
  }
  
  return(true);
}

function search_contacts($search)
{
  // Search the contacts table with search string $search.

  // Returns: false on error, or an array of the form
  //  $ret['count']: Number of entries
  //  $ret[#]['contactid']: Contact ID for entry #
  //  $ret[#]['givenname']: Given Name for entry #
  //  $ret[#]['sn']: Surname for entry #
  //  $ret[#]['mail']: Email for entry #

  global $config;
  global $dbc;

  $found = array();
  $ret = array();
  $ret['count'] = 0;

  // Retrieve all potential matches from the database.  This is similar
  // to what we do in directory_query().  We don't look at mail here
  // because that should have been done via a call to lookup_user_by_email,
  // though maybe that should be consolidated here.
  
  // First, we try to match all tokens, then any token

  if($config['contactsearchOR'])
    $sarray = array(' AND', ' OR');
  else
    $sarray = array(' AND');
    
  foreach($sarray as $op)
  {
    // First, we try just the tokens.  Then, we try wildcarding them.
  
    foreach(array('', '%') as $wildcard)
    {
      $tokens = explode(" ", $search);

      $n = count($tokens) - 1;

      // Iterate through any "middle" tokens, trying them in both
      // givenname and sn.  Eg, for James Michael Patrick Smith we
      // would try (James Michael Patrick) (Smith), (James Michael)
      // (Patrick Smith). and (James) (Michael Patrick Smith).
	      	      
      for($i = 0;$i <= $n;$i++)    // <= $n allows us to handle single tokens
      {
	// Try set1 as gn, set2 as sn as well as set2 as gn, set1 as sn

	$query = "SELECT contactid, givenname, sn, mail
                  FROM mrsbs_contacts
                  WHERE (";
	
	for($j = 0;$j <= $i;$j++)
	  $query .= ($j > 0 ? $op : '') . ' givenname LIKE '
	    . sstr($tokens[$j] . $wildcard);
	
	for($j = $i+1;$j < $n;$j++)  // Don't count $n since we started at 0
	  $query .= ($j > 0 ? $op : '') . ' sn LIKE '
	    . sstr($tokens[$j] . $wildcard);
	
	$query .= ") OR (";
	
	for($j = 0;$j <= $i;$j++)
	  $query .= ($j > 0 ? $op : '') . ' sn LIKE '
	    . sstr($tokens[$j] . $wildcard);
	
	for($j = $i+1;$j < $n;$j++)
	  $query .= ($j > 0 ? $op : '') . ' givenname LIKE '
	    . sstr($tokens[$j] . $wildcard);

	$query .= ")
                   ORDER BY sn";

	$records = $dbc->Execute($query);

	if($records)
	{
	  while(!$records->EOF)
	  {
	    // We don't want to return the same record twice, so we track
	    // what we found
	    
	    if(!isset($found[$records->fields['contactid']]))
	    {
	      $found[$records->fields['contactid']] = 1;

	      $c = $ret['count'];
	      $ret[$c]['contactid'] = $records->fields['contactid'];
	      $ret[$c]['givenname'] = hstr($records->fields['givenname']);
	      $ret[$c]['sn'] = hstr($records->fields['sn']);
	      $ret[$c]['mail'] = hstr($records->fields['mail']);
	      $ret['count']++;
	    }
	    
	    $records->MoveNext();
	  }
	}
      }
    }
  }
		
  return($ret);
}

function send_failed_to_schedule($mtgid)
{
  // Notify the owner and host of $mtgid that the event failed to schedule.

  // Returns: true if fully successful, false otherwise.

  global $tx;
  global $config;

  $ret = false;
    
  // First get the meeting info

  $mtg = get_meeting_info($mtgid);

  if($mtg)
  {
    $ret = true;

    // Prepare return addresses
    
    $sender = render_name("mailfrom",
			  $mtg['hostgivenname'],
			  $mtg['hostsn'],
			  $mtg['hostmail']);

    $recipients =  render_name("mail",
			     $mtg['contactgivenname'],
			     $mtg['contactsn'],
			     $mtg['contactmail']);

    $headers = array("From" => $sender,
		     "To" => $recipients,
		     "Subject" => $tx['in.subject.failed'] .
		     ": " . $mtg['summary']);

    $body = wordwrap($tx['in.failed']) . "\n";

    $body .= $tx['hs.reply.mtg'];
    $body .= " <" . build_uri("/schedule/review.php?mtgid=" . $mtg['mtgid']) .
      ">\n\n";
      
    // Send the message
	
    $mailer = &Mail::factory($config['mailengine'], $config['mailparams']);
	
    if(!$mailer->send($recipients, $headers, $body))
      $ret = false;

    // Update history
      
    record_history($mtgid, $mtg['contactid'], "FAIL",
		   $tx['hi.fail'] . $mtg['contactmail']);
  }

  return($ret);
}

function send_host_reply($inviteid, $note)
{
  // Send a message to the host that $inviteid has been replied to,
  // with optional $note.

  // Returns: true if fully successful, false otherwise.

  global $tx;
  global $config;

  $ret = false;
  
  // First get the invite info

  $inv = get_invite_info($inviteid);

  if($inv)
  {
    // Assemble the message header and body
      
    $sender = render_name("mailfrom",
			  $inv['givenname'],
			  $inv['sn'],
			  $inv['mail']);

    $recipients = render_name("mail",
			      $inv['mtg']['contactgivenname'],
			      $inv['mtg']['contactsn'],
			      $inv['mtg']['contactmail']);

    // Copy host if delegated
    
    if($inv['mtg']['contactid'] != $inv['mtg']['hostid'])
      $recipients .= ", " . render_name("mail",
					$inv['mtg']['hostgivenname'],
					$inv['mtg']['hostsn'],
					$inv['mtg']['hostmail']);
      
    $headers = array("From" => $sender,
		     "To" => $recipients,
		     "Subject" => $tx['hs.reply.subject'] . ": " .
		                  $inv['mtg']['summary']);

    $body = $tx['hs.reply.body'];

    $body .= render_name("compact", $inv['givenname'], $inv['sn'],
			 $inv['mail']) . ": ";

    switch($inv['reply'])
    {
    case 'A':
      $body .= $tx['rp.grid.avail'];
      break;
    case 'D':
      $body .= $tx['rp.notattend'];
      break;
    default:
      $body .= "?";
      break;
    }

    $body .= "\n\n";
    
    if($note != "")
      $body .= wordwrap(stripslashes($note)) . "\n\n";

    /*
     No need to include this link since meeting replies can be viewed
     via the other link, and this one creates confusion with the option
     to "view or edit your reply", where "your" really means the person
     who replied

    $body .= $tx['hs.reply.status'];
    $body .= " <" . build_uri("/reply/status.php?inviteid=" . $inviteid) .
      ">\n\n";
    */
    
    $body .= $tx['hs.reply.status'];
    $body .= " <" . build_uri("/schedule/review.php?mtgid=" . $inv['mtgid']) .
      ">\n\n";
      
    // Send the message

    $mailer = &Mail::factory($config['mailengine'], $config['mailparams']);

    if($mailer->send($recipients, $headers, $body))
      $ret = true;
  }

  return($ret);
}

function send_invitations($mtgid, $note="", $recipids=false)
{
  // Send invitations for $mtgid, with optional $note.  If $recipids
  // is provided as a list of contactids, only those contactids are
  // notified.

  // Returns: true if fully successful, false otherwise.

  global $tx;
  global $config;

  $ret = false;
  
  // First get the meeting info

  $mtg = get_meeting_info($mtgid);

  if($mtg)
  {
    $ret = true;

    // Prepare return addresses
    
    $sender = render_name("mailfrom",
			  $mtg['hostgivenname'],
			  $mtg['hostsn'],
			  $mtg['hostmail']);

    if($mtg['contactid'] != $mtg['hostid'])
    {
      // Set replyto to the contact instead of the host
      
      $replyto = render_name("mail",
			     $mtg['contactgivenname'],
			     $mtg['contactsn'],
			     $mtg['contactmail']);
    }
    else
      $replyto = "";
	
    for($i = 0;$i < $mtg['invitees']['count'];$i++)
    {
      if(!$recipids || in_array($mtg['invitees'][$i]['contactid'], $recipids))
      {
	// Assemble the message header and body
      
	$recipients = render_name("mail",
				  $mtg['invitees'][$i]['givenname'],
				  $mtg['invitees'][$i]['sn'],
				  $mtg['invitees'][$i]['mail']);

	// Copy anyone delegated to reply to invites

	$dels = get_delegate_reply_for($mtg['invitees'][$i]['contactid']);

	if($dels && $dels['count'] > 0)
	{
	  for($j = 0;$j < $dels['count'];$j++)
	  {
	    $recipients .= ", " . render_name("mail",
					      $dels[$j]['givenname'],
					      $dels[$j]['sn'],
					      $dels[$j]['mail']);
	  }
	}

	$headers = array("From" => $sender,
			 "To" => $recipients,
			 "Subject" => (($mtg['status'] == "i") ?
				       $tx['in.subject.canceled'] :
				       $tx['in.subject']) .
			               ": " . $mtg['summary']);

	if($replyto != "")
	  $headers["Reply-To"] = $replyto;

	$body = "";
	
	if($note != "")
	  $body .= wordwrap(stripslashes($note)) . "\n\n";

	if($mtg['status'] == "i")
	  $body .= $tx['in.canceled'] . "\n";
	else
	{
	  $body .= render_name("compact", $mtg['contactgivenname'],
			       $mtg['contactsn'], $mtg['contactmail']) .
	    ' ' . $tx['in.invited'];
	
	  if($mtg['invitees'][$i]['status'] == 'O')
	    $body .= $tx['in.optional'];
	  elseif($mtg['invitees'][$i]['status'] == 'N')
	    $body .= $tx['in.notattend'];

	  $body .= "\n";
      
	  $body .= $tx['in.viewreply'];
	  $body .= " <" . build_uri("/reply/reply.php?inviteid=" .
				    $mtg['invitees'][$i]['inviteid']) . ">\n\n";
	}
      
	$body .= $tx['in.info'];
	$body .= wordwrap($mtg['description']) . "\n\n";
	$body .= $tx['in.inviteid'] . ": " . $mtg['invitees'][$i]['inviteid']
	  . "\n";
	
	// Send the message
	
	$mailer = &Mail::factory($config['mailengine'], $config['mailparams']);
	
	if(!$mailer->send($recipients, $headers, $body))
	  $ret = false;

	// Update history
      
	record_history($mtgid, $mtg['contactid'], "INV",
		       $tx['hi.inv'] . $mtg['invitees'][$i]['mail']);
      }
    }
  }

  return($ret);
}

function send_notifications($mtgid, $note="", $recipids=false, $changed=0)
{
  // Send notifications (ICS invites) for $mtgid, with optional $note.
  // If $recipids is provided as a list of contactids, only those
  // contactids are notified.  If $changed is provided, notification
  // indicates a previous message changed as appropriate.

  // Returns: true if fully successful, false otherwise.

  global $config, $tx;

  $ret = false;
  
  // First get the meeting info

  $mtg = get_meeting_info($mtgid);

  if($mtg)
  {
    $ret = true;

    // Prepare return addresses
    
    $sender = render_name("mailfrom",
			  $mtg['hostgivenname'],
			  $mtg['hostsn'],
			  $mtg['hostmail']);

    if($mtg['contactid'] != $mtg['hostid'])
    {
      // Set replyto to the contact instead of the host
      
      $replyto = render_name("mail",
			     $mtg['contactgivenname'],
			     $mtg['contactsn'],
			     $mtg['contactmail']);
    }
    else
      $replyto = "";
	
    // We iterate once per recipient so as to customize some aspects
    // of the message for that recipient.

    for($i = -1;$i < $mtg['invitees']['count'];$i++)
    {
      // The first time through (-1), we send to the meeting host (if we're
      // sending to everyone), however, we don't send twice to the host
      // if the host is also an invitee
      
      if((!$recipids || in_array($mtg['invitees'][$i]['contactid'], $recipids))
	 &&
	 ($i == -1 || ($mtg['invitees'][$i]['contactid'] != $mtg['hostid'])))
      {
	$ics = generate_ics($mtg, $i);

	// We are now ready to mail this invite to its recipient.
	// Assemble the message header and body.  This will be
	// a multipart message.

	if($i == -1)
	  $recipients = render_name("mail",
				    $mtg['hostgivenname'],
				    $mtg['hostsn'],
				    $mtg['hostmail']);
	else
	  $recipients = render_name("mail",
				    $mtg['invitees'][$i]['givenname'],
				    $mtg['invitees'][$i]['sn'],
				    $mtg['invitees'][$i]['mail']);
	
	$boundary = md5(time());

	$subpre = $tx['in.subject.confirmed'];

	if($changed)
	  $subpre = $tx['in.subject.adjusted'];
	elseif($mtg['status'] == "s")
	  $subpre = $tx['in.subject.canceled'];

	$headers = array("From" => $sender,
			 "To" => $recipients,
			 "Subject" => $subpre . ": " . $mtg['summary'],
			 "MIME-Version" => "1.0",
			 "Content-Type" => 'multipart/mixed; boundary="' .
			                   $boundary .'";',
			 "Content-Transfer-Encoding" => "7bit");
	
	if($replyto != "")
	  $headers["Reply-To"] = $replyto;

	$body = 'This is a multipart MIME message.

--' . $boundary . '
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 7bit

' . (($note != "") ? (wordwrap(stripslashes($note)) . "\n\n") : '');

	if($mtg['status'] == "s")
	  $body .= $tx['in.canceled'];
	elseif($changed)
	{
	  switch($changed)
	  {
	  case 1:
	    $body .= $tx['in.rescheduled'];
	    break;
	  case 2:
	    $body .= $tx['in.relocated'];
	    break;
	  case 3:
	    $body .= $tx['in.reschedreloc'];
	    break;
	  }
	}
	else
	  $body .= $tx['in.scheduled'];

	$body .= '
';
	
	if($i > -1)
	{
	  if($mtg['invitees'][$i]['status'] == 'O')
	    $body .= $tx['in.optional'];
	  elseif($mtg['invitees'][$i]['status'] == 'N')
	    $body .= $tx['in.notattend'];
	}
	else
	  $body .= $tx['in.host'];
	
	$body .=  $tx['in.info'] . wordwrap($mtg['description']) . '

 ' . $tx['sd.host'] . ': ' .
	  render_name("compact", $mtg['hostgivenname'],
		      $mtg['hostsn'], $mtg['hostmail']) . '
 ' . $tx['sd.invitees'] . ':
';
	for($j = 0;$j < $mtg['invitees']['count'];$j++)
	  $body .= '  ' . render_name("full",
				      $mtg['invitees'][$j]['givenname'],
				      $mtg['invitees'][$j]['sn'],
				      $mtg['invitees'][$j]['mail'])
	    . "\n";

	$body .= '
 ' . $tx['sd.time.start'] . ': ' . strftime('%c %Z', $mtg['scheduledfor']) . '
 ' . $tx['sd.time.end'] . ': ' .
	  strftime('%c %Z', ($mtg['scheduledfor'] + ($mtg['duration'] * 60))) . '
 ' . $tx['sd.loc'] . ': ' .render_location($mtg['location'],
					  $mtg['locinfo'],
					  $mtg['locationstatus']) . '
';

	if($i > -1)
	  $body .= '
' . $tx['in.viewinfo'] . build_uri('/reply/status.php?inviteid='
				   . $mtg['invitees'][$i]['inviteid']) . '

' . $tx['in.inviteid'] . ": " . $mtg['invitees'][$i]['inviteid'];
	else
	  $body .= '
' . $tx['in.viewinfo'] . build_uri('/schedule/review.php?mtgid='
				   . $mtg['mtgid']);
	
	$body .= '

--' . $boundary . '
Content-Type: text/calendar; method=REQUEST; name="meeting.ics"
Content-Transfer-Encoding: 7bit

' . $ics . '

--' . $boundary . '--
';

	// Send the message

	$mailer = &Mail::factory($config['mailengine'], $config['mailparams']);
	
	if(!$mailer->send($recipients, $headers, $body))
	  $ret = false;

	// Update history
	
	record_history($mtgid, $mtg['contactid'], "ICS",
		       $tx['hi.ics'] .
		       ($i == -1 ? $mtg['hostmail']
			: $mtg['invitees'][$i]['mail']));
      }
    }
  }
 
  return($ret);
}

function sort_meeting_locations($mtg)
{
  // Sort the meeting locations for $mtg according to the received
  // replies.  $mtg is an array of the form returned by
  // get_meeting_info.

  // Returns: An array, sorted from most preferred to least, of the form
  //  $ret[#]['locationid']: Location ID of #
  //  $ret[#]['desc']: Description of #
  //  There may be additional fields used to sort the windows.
  // Unavailable locations are omitted.

  global $config, $config;
  
  // Obtain the info for each invite

  $invs = array();
  $invs['count'] = $mtg['invitees']['count'];

  for($i = 0;$i < $mtg['invitees']['count'];$i++)
    $invs[$i] = get_invite_info($mtg['invitees'][$i]['inviteid']);

  // Iterate through each location

  $locs = array();
  $locs['count'] = 0;

  for($i = 0;$i < $mtg['locations']['count'];$i++)
  {
    if($mtg['locations'][$i]['pref'] > 0)
    {
      // Skip locations marked "not acceptable"

      $t = $locs['count'];
      $locs[$t] = array();
      $locs[$t]['locationid'] = $mtg['locations'][$i]['locationid'];
      $locs[$t]['capacity'] = $mtg['locations'][$i]['capacity'];
      $locs[$t]['desc'] = $mtg['locations'][$i]['desc'];
      $locs[$t]['pref'] = $mtg['locations'][$i]['pref'];
      $locs[$t]['rA'] = 0;     // Unacceptable for required invitees
      $locs[$t]['oA'] = 0;     // Unacceptable for optional invitees
      $locs[$t]['rp'] = 0;     // Preferred for required invitees
      $locs[$t]['op'] = 0;     // Preferred for optional invitees
      $locs[$t]['invitees'] = $invs['count'];  // Used for sorting
      $locs['count']++;
      
      if($config['debug'])
	print "Location: " . $locs[$t]['desc'] .
	  ($mtg['locations'][$i]['pref'] == 2 ? " (Preferred)" : "") . "\n";

      // If the host marked this as preferred, increment the pref count
      if($mtg['locations'][$i]['pref'] == 2)
	$locs[$t]['rp']++;
      
      for($k = 0;$k < $invs['count'];$k++)
      {
	if(isset($invs['locations'][$locs[$t]['locationid']]['pref']))
	{
	  if($invs['status'] == 'R' &&
	     $invs['locations'][$locs[$t]['locationid']]['pref'] == 2)
	  {
	    if($config['debug'])
	      print " -> Preferred (" . $invs[$k]['mail'] . ")\n";
	    $locs[$t]['rp']++;
	  }

	  if($invs['status'] == 'R' &&
	     $invs['locations'][$locs[$t]['locationid']]['pref'] == 0)
	  {
	    if($config['debug'])
	      print " -> Not acceptable (" . $invs[$k]['mail'] . ")\n";
	    $locs[$t]['rA']++;
	  }

	  if($invs['status'] == ')' &&
	     $invs['locations'][$locs[$t]['locationid']]['pref'] == 2)
	  {
	    if($config['debug'])
	      print " -> Preferred (O:" . $invs[$k]['mail'] . ")\n";
	    $locs[$t]['op']++;
	  }

	  if($invs['status'] == ')' &&
	     $invs['locations'][$locs[$t]['locationid']]['pref'] == 0)
	  {
	    if($config['debug'])
	      print " -> Not acceptable (O:" . $invs[$k]['mail'] . ")\n";
	    $locs[$t]['oA']++;
	  }
	}
	else
	{
	  if($config['debug'])
	    print " -> No preference indicated (" . $invs[$k]['mail'] . ")\n";
	}
      }
    }
    else
    {
      if($config['debug'])
	print "Location: " . $mtg['locations'][$i]['desc'] .
	  " (Not acceptable)\n";
    }
  }

  // push all $locs where rA == 0 into the candidates array
  
  $candidates = array();

  for($i = 0;$i < $locs['count'];$i++)
  {
    if($locs[$i]['rA'] == 0)
    {
      if($config['debug'])
	print "Adding $i to candidates\n";
      $candidates[] = $locs[$i];
    }
  }

  // then sort the array according to our magic formula

  usort($candidates, "sort_meeting_locations_cmp");

  for($i = 0;$i < count($candidates);$i++)
  {
    if($config['debug'])
      print $i . ": " . $candidates[$i]['desc'] . "\n";
  }
  
  return($candidates);
}

function sort_meeting_locations_cmp($a, $b)
{
  // Comparison routine to sort locations via usort.

  // Returns: -1 if $a is more preferred than $b or 1 if $b is more
  // preferred than $a.  $a and $b will never be considered equivalent
  // since PHP > 4.1.0 won't preserve the original order.

  // Start with required not acceptable

  if($a['rA'] != $b['rA'])
    return(($a['rA'] < $b['rA']) ? -1 : 1);
  
  // Next check fewest optional not acceptable

  if($a['oA'] != $b['oA'])
    return(($a['oA'] < $b['oA']) ? -1 : 1);

  // Next check greatest total preferred

  if(($a['rp'] + $a['op']) != ($b['rp'] + $b['op']))
    return((($a['rp'] + $a['op']) > ($b['rp'] + $b['op'])) ? -1 : 1);

  // Next check greatest required preferred

  if($a['rp'] != $b['rp'])
    return(($a['rp'] > $b['rp']) ? -1 : 1);

  // At this point, $a and $b are essentially equivalent.
  // Check original preference.

  if($a['pref'] != $b['pref'])
    return(($a['pref'] < $b['pref']) ? -1 : 1);

  // With no other indications, go by room size.  Prefer the room
  // larger than the number of invitees, but by the least.

  if(($a['capacity'] >= $a['invitees']) && ($b['capacity'] >= $b['invitees']))
    return(($a['capacity'] < $b['capacity']) ? -1 : 1);

  if(($a['capacity'] >= $a['invitees']) && ($b['capacity'] < $b['invitees']))
    return(-1);

  if(($a['capacity'] < $a['invitees']) && ($b['capacity'] >= $b['invitees']))
    return(1);

  // If both candidates are too small, go with the larger one
  
  if(($a['capacity'] < $a['invitees']) && ($b['capacity'] >= $b['invitees']))
    return(($a['capacity'] > $b['capacity']) ? -1 : 1);
    
  // If we're *still* tied at this point, the rooms are basically identical
  // and it doesn't matter which we pick.  We'll go with the lower locid.

  return(($a['locationid'] < $b['locationid']) ? -1 : 1);
}

function sort_meeting_windows($mtg)
{
  // Sort the meeting windows for $mtg according to the received replies.
  // $mtg is an array of the form returned by get_meeting_info.

  // Returns: An array, sorted from most preferred to least, of the form
  //  $ret[#]['windowid']: WindowID of #
  //  $ret[#]['begin']: Begin time of #
  //  $ret[#]['end']: End time of #
  //  There may be additional fields used to sort the windows.
  // Unavailable windows are omitted.

  global $config, $config;
  
  // Obtain the info for each invite

  $invs = array();
  $invs['count'] = $mtg['invitees']['count'];

  for($i = 0;$i < $mtg['invitees']['count'];$i++)
    $invs[$i] = get_invite_info($mtg['invitees'][$i]['inviteid']);

  // Iterate through each window

  $ints = array();
  $ints['count'] = 0;
  
  for($i = 0;$i < $mtg['windows']['count'];$i++)
  {
    if($config['debug'])
      print "pref=" . $mtg['windows'][$i]['pref'] . ", " .
	$mtg['windows'][$i]['begin'] . "->" .
	$mtg['windows'][$i]['end'] . "\n";

    // Break each window into possible intervals
    // eg: a 90 minute window has 3 possible intervals for a 60 minute
    // event based on $config['schedint'] being 15 minutes (0-60, 15-75,
    // 30-90)
    
    for($j = $mtg['windows'][$i]['begin'];
	$j <= ($mtg['windows'][$i]['end'] - ($mtg['duration'] * 60));
	$j += ($config['schedint'] * 60))
    {
      $ej = $j + ($mtg['duration'] * 60);
      
      // An interval is a candidate only if all required invitees who
      // have replied are available during this interval.

      // A "not attending" reply is ignored for calculation purposes,
      // since a declined invitation is different from unavailability.

      $t = $ints['count'];
      $ints[$t] = array();
      $ints[$t]['windowid'] = $mtg['windows'][$i]['windowid'];
      $ints[$t]['pref'] = $mtg['windows'][$i]['pref'];
      $ints[$t]['begin'] = $j;
      $ints[$t]['end'] = $ej;
      $ints[$t]['rA'] = 0;   // Unavailable required invitees
      $ints[$t]['oA'] = 0;   // Unavailable optional invitees
      $ints[$t]['rP'] = 0;   // Not preferred (required invitees)
      $ints[$t]['oP'] = 0;   // Not preferred (optional invitees)
      $ints[$t]['rp'] = 0;   // Preferred (required invitees)
      $ints[$t]['op'] = 0;   // Preferred (optional invitees)
      $ints['count']++;
      
      if($config['debug'])
	print " Interval $t: " . date("r",$j) . " -> " . date("r",$ej) . "\n";

      for($k = 0;$k < $invs['count'];$k++)
      {
	if($invs[$k]['status'] == 'R'
	   && determine_availability($invs[$k]['windows'], $j, $ej) == "A")
	{
	  if($config['debug'])
	    print " -> Not available (" . $invs[$k]['mail'] . ")\n";
	  $ints[$t]['rA']++;
	}

	if($invs[$k]['status'] == 'O'
	   && determine_availability($invs[$k]['windows'], $j, $ej) == "A")
	{
	  if($config['debug'])
	    print " -> Not available (O:" . $invs[$k]['mail'] . ")\n";
	  $ints[$t]['oA']++;
	}
	
	if($invs[$k]['status'] == 'R'
	   && determine_availability($invs[$k]['windows'], $j, $ej) == "P")
	{
	  if($config['debug'])
	    print " -> Not preferred (" . $invs[$k]['mail'] . ")\n";
	  $ints[$t]['rP']++;
	}

	if($invs[$k]['status'] == 'O'
	   && determine_availability($invs[$k]['windows'], $j, $ej) == "P")
	{
	  if($config['debug'])
	    print " -> Not preferred (O:" . $invs[$k]['mail'] . ")\n";
	  $ints[$t]['oP']++;
	}
	
	if($invs[$k]['status'] == 'R'
	   && determine_availability($invs[$k]['windows'], $j, $ej) == "p")
	{
	  if($config['debug'])
	    print " -> Preferred (" . $invs[$k]['mail'] . ")\n";
	  $ints[$t]['rp']++;
	}

	if($invs[$k]['status'] == 'O'
	   && determine_availability($invs[$k]['windows'], $j, $ej) == "p")
	{
	  if($config['debug'])
	    print " -> Preferred (O:" . $invs[$k]['mail'] . ")\n";
	  $ints[$t]['op']++;
	}
      }
    }
  }

  // push all $ints where rA == 0 into the candidates array, but not if
  // the begin time has already passed
  
  $candidates = array();

  for($i = 0;$i < $ints['count'];$i++)
  {
    if($ints[$t]['begin'] > time())
    {
      if($ints[$i]['rA'] == 0)
      {
	if($config['debug'])
	  print "Adding $i to candidates\n";
	$candidates[] = $ints[$i];
      }
    }
    else
    {
      if($config['debug'])
	print "Not addint $i to candidates, start time has passed\n";
    }
  }

  // then sort the array according to our magic formula

  usort($candidates, "sort_meeting_windows_cmp");

  for($i = 0;$i < count($candidates);$i++)
  {
    if($config['debug'])
      print $i . ": " . date("r",$candidates[$i]['begin']) . " -> " .
	date("r", $candidates[$i]['end']) . "\n";
  }
  
  return($candidates);
}
     
function sort_meeting_windows_cmp($a, $b)
{
  // Comparison routine to sort intervals via usort.

  // Returns: -1 if $a is more preferred than $b or 1 if $b is more
  // preferred than $a.  $a and $b will never be considered equivalent
  // since PHP > 4.1.0 won't preserve the original order.

  // By the time we get here, unavailable required should have been filtered,
  // but we'll check just in case.

  if($a['rA'] != $b['rA'])
    return(($a['rA'] < $b['rA']) ? -1 : 1);
  
  // Next check fewest optional unavailable

  if($a['oA'] != $b['oA'])
    return(($a['oA'] < $b['oA']) ? -1 : 1);

  // Next check fewest required not preferred

  if($a['rP'] != $b['rP'])
    return(($a['rP'] < $b['rP']) ? -1 : 1);

  // Next check fewest optional not preferred

  if($a['oP'] != $b['oP'])
    return(($a['oP'] < $b['oP']) ? -1 : 1);
  
  // Next check greatest total preferred

  if(($a['rp'] + $a['op']) != ($b['rp'] + $b['op']))
    return((($a['rp'] + $a['op']) > ($b['rp'] + $b['op'])) ? -1 : 1);

  // Next check greatest required preferred

  if($a['rp'] != $b['rp'])
    return(($a['rp'] > $b['rp']) ? -1 : 1);

  // Next check greatest optional preferred
  // Since we check greatest total, we probably don't need to do this

  if($a['op'] != $b['op'])
    return(($a['op'] > $b['op']) ? -1 : 1);

  // At this point, $a and $b are essentially equivalent.
  // Check original preference.

  if($a['pref'] != $b['pref'])
    return(($a['pref'] < $b['pref']) ? -1 : 1);

  // Finally look at start time, and select the earliest time.
  // We shouldn't get two identical start times since that would
  // represent the same interval, and in that case it wouldn't
  // matter which we picked.

  return(($a['begin'] < $b['begin']) ? -1 : 1);
}

function sstr($s)
{
  // Make $s safe for substituting into an SQL query.

  // Returns: A sanitized string.

  global $dbc;

  return($dbc->qstr($s, get_magic_quotes_gpc()));
}

function update_acls($rtype, $rid, $acls)
{
  // Update the ACLs for the resource of type $rtype and id $rid to $acls,
  // which is an array of the form
  //  $a['a']: Numeric permission for "all"
  //  $a['g']['count']: Number of group permissions
  //  $a['g'][#]['who']: Name of group permission #
  //  $a['g'][#]['perm']: Numeric permission for #
  //  $a['u']['count']: Number of user permissions
  //  $a['u'][#]['who']: Name of user permission #
  //  $a['u'][#]['perm']: Numeric permission for #
  // Any existing ACLs for $rtype/$rid will be removed.

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $dbc->StartTrans();

  $sql = $dbc->Prepare('DELETE FROM mrsbs_acls
                        WHERE resourcetype=' . $dbc->Param('a') . '
                        AND resourceid=' . $dbc->Param('b'));
  
  $dbc->Execute($sql, array($rtype, $rid));

  if(isset($acls['a']))
  {
    $sql = $dbc->Prepare('INSERT INTO mrsbs_acls
                          (resourcetype, resourceid, acltype, who, perm)
                          VALUES ("L", ' . $dbc->Param('a') . ', "A", "", ' .
	       	                  $dbc->Param('b') . ')');
    
    $dbc->Execute($sql, array($rid, $acls['a']));
  }

  if(isset($acls['g']))
  {
    for($i = 0;$i < $acls['g']['count'];$i++)
    {
      $sql = $dbc->Prepare('INSERT INTO mrsbs_acls
                            (resourcetype, resourceid, acltype, who, perm)
                            VALUES ("L", ' .  $dbc->Param('a'). ', "G", ' .
		                    $dbc->Param('b') . ',' .
		                    $dbc->Param('c') . ')');
      
      $dbc->Execute($sql, array($rid,
				$acls['g'][$i]['who'],
				$acls['g'][$i]['perm']));
    }
  }

  if(isset($acls['u']))
  {
    for($i = 0;$i < $acls['u']['count'];$i++)
    {
      $sql = $dbc->Prepare('INSERT INTO mrsbs_acls
                            (resourcetype, resourceid, acltype, who, perm)
                            VALUES ("L", ' .  $dbc->Param('a'). ', "U", ' .
		                    $dbc->Param('b') . ',' .
		                    $dbc->Param('c') . ')');

      $dbc->Execute($sql, array($rid,
				$acls['u'][$i]['who'],
				$acls['u'][$i]['perm']));
    }
  }

  return($dbc->CompleteTrans());
}

function update_contact($contactid, $givenname, $sn, $mail)
{
  // Update $contactid, with $givenname, $sn, $mail.

  // Returns: true if fully successful, false otherwise.

  global $dbc, $config;

  $m = $mail;
  
  if($config['demomode'])
  {
    // Only update mail if it differs from what's in the db, since we've
    // obscured it.

    $ct = get_contact($contactid);

    if($mail != $ct['mail'])
      $m = maybe_obscure_email($mail);
  }

  $sql = $dbc->Prepare('UPDATE mrsbs_contacts SET          
                        givenname=' . $dbc->Param('a') . ',
                        sn=' . $dbc->Param('b') . ',
                        mail=' . $dbc->Param('c') . '
                        WHERE contactid=' . $dbc->Param('d'));
  
  return($dbc->Execute($sql, array($givenname, $sn, $m, $contactid)));
}

function update_delegates($createon, $createoff, $replyon, $replyoff,
			  $removes)
{
  // Update delegate permissions based on $createon, $createoff,
  // $replyon, $replyoff, and $removes.

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $dbc->StartTrans();

  // Process create=Y
  
  for($i = 0;$i < count($createon);$i++)
  {
    $ids = preg_split('/-/', $createon[$i]);

    $sql = $dbc->Prepare('UPDATE mrsbs_delegates
                          SET pcreate="Y"
                          WHERE contactid=' . $dbc->Param('a') . '
                          AND delegateid=' . $dbc->Param('b'));

    $dbc->Execute($sql, array($ids[0], $ids[1]));
  }

  // Process create=N
  
  for($i = 0;$i < count($createoff);$i++)
  {
    $ids = preg_split('/-/', $createoff[$i]);

    $sql = $dbc->Prepare('UPDATE mrsbs_delegates
                          SET pcreate="N"
                          WHERE contactid=' . $dbc->Param('a') . '
                          AND delegateid=' . $dbc->Param('b'));

    $dbc->Execute($sql, array($ids[0], $ids[1]));
  }
  
  // Process reply=Y
  
  for($i = 0;$i < count($replyon);$i++)
  {
    $ids = preg_split('/-/', $replyon[$i]);

    $sql = $dbc->Prepare('UPDATE mrsbs_delegates
                          SET preply="Y"
                          WHERE contactid=' . $dbc->Param('a') . '
                          AND delegateid=' . $dbc->Param('b'));

    $dbc->Execute($sql, array($ids[0], $ids[1]));
  }

  // Process reply=N
  
  for($i = 0;$i < count($replyoff);$i++)
  {
    $ids = preg_split('/-/', $replyoff[$i]);

    $sql = $dbc->Prepare('UPDATE mrsbs_delegates
                          SET preply="N"
                          WHERE contactid=' . $dbc->Param('a') . '
                          AND delegateid=' . $dbc->Param('b'));

    $dbc->Execute($sql, array($ids[0], $ids[1]));
  }

  // Process removes
  
  for($i = 0;$i < count($removes);$i++)
  {
    $ids = preg_split('/-/', $removes[$i]);

    $sql = $dbc->Prepare('DELETE FROM mrsbs_delegates
                          WHERE contactid=' . $dbc->Param('a') . '
                          AND delegateid=' . $dbc->Param('b'));

    $dbc->Execute($sql, array($ids[0], $ids[1]));
  }
  
  return($dbc->CompleteTrans());
}

function update_invitee_reply($inviteid, $attend, $slots, $locs, $note)
{
  // Update the reply attached to $inviteid as follows:
  // $attend: boolean to indicate attending (true) or declining (false)
  // $slots: array of the form
  //  ['count']: number of slots
  //  [#]['begin']: slot start time (time_t)
  //  [#]['end']: slot end time (time_t)
  //  [#]['pref']: slot preference ([p]ref, [a]vail, not [P]ref, not [A]vail)
  // $locs: array of the form
  //  ['count']: number of locations
  //  [#]['locid']: location ID
  //  [#]['pref']: 0=unacceptable, 1=acceptable, 2=preferred
  // $note: Comment to store in history

  // Returns: true if fully successful, false otherwise.

  global $dbc;
  global $tx;

  $dbc->StartTrans();

  $inv = get_invite_info($inviteid);

  if($inv)
  {  
    // First, toss any previous records

    $sql = $dbc->Prepare('DELETE FROM mrsbs_reply_locations
                          WHERE inviteid=' . $dbc->Param('a'));

    $dbc->Execute($sql, array($inviteid));

    $sql = $dbc->Prepare('DELETE FROM mrsbs_reply_windows
                          WHERE inviteid=' . $dbc->Param('a'));

    $dbc->Execute($sql, array($inviteid));

    // Next, update the invitee table

    if($attend)
    {
      $sql = $dbc->Prepare('UPDATE mrsbs_invitees SET reply="A"
                            WHERE inviteid=' . $dbc->Param('a'));

      $dbc->Execute($sql, array($inviteid));
      
      // Update the reply window table
      
      for($i = 0;$i < $slots['count'];$i++)
      {
	$sql = 'INSERT INTO mrsbs_reply_windows
                (inviteid, status, begin, end)
                VALUES (' . sstr($inviteid) . ','
	                  . sstr($slots[$i]['pref']) . ','
	                  . $dbc->DBTimeStamp($slots[$i]['begin']) . ','
			  . $dbc->DBTimeStamp($slots[$i]['end']) . ')';

	$dbc->Execute($sql);
      }
      
      // Update the location table
      
      for($i = 0;$i < $locs['count'];$i++)
      {
	$sql = $dbc->Prepare('INSERT INTO mrsbs_reply_locations
                              (inviteid, locationid, pref)
                              VALUES (' . $dbc->Param('a') . ','
	                                . $dbc->Param('b') . ','
			                . $dbc->Param('c')  . ')');

	$dbc->Execute($sql, array($inviteid,
				  $locs[$i]['locid'],
				  $locs[$i]['pref']));
      }
    }
    else
    {
      $sql = $dbc->Prepare('UPDATE mrsbs_invitees SET reply="D"
                            WHERE inviteid=' . $dbc->Param('a'));
      
      $dbc->Execute($sql, array($inviteid));
    }

    record_history($inv['mtg']['mtgid'], $inv['contactid'], "RSVP",
		   $tx['hi.rsvp'] . " (" . $tx['in.inviteid'] . " " .
		   $inv['inviteid'] . ") " . $tx['hi.comment'] . ": " . $note);

    // If the meeting previously failed to schedule, reset the meeting status

    if($mtg['status'] == 'F')
      update_meeting_status($mtg['mtgid'], "I");
  }
  else
    $dbc->FailTrans();

  return($dbc->CompleteTrans());  
}

function update_location($locid, $desc, $capacity, $contactid, $system, $acl)
{
  // Update $locid, with description $desc, $capacity, owner $contactid,
  // $system type, and $acl (of the form used by update_acls).

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $dbc->StartTrans();
  
  $sql = $dbc->Prepare('UPDATE mrsbs_locations SET          
                        description=' . $dbc->Param('a') . ',
                        capacity=' . $dbc->Param('b') . ',
                        contactid=' . $dbc->Param('c') . ',
                        system=' . $dbc->Param('d') . '
                        WHERE locationid=' . $dbc->Param('e'));
  
  if($dbc->Execute($sql, array($desc, $capacity, $contactid, $system, $locid)))
    update_acls("L", $locid, $acl);

  return($dbc->CompleteTrans());
}

function update_meeting_host($mtgid, $hostid)
{
  // Set the host of $mtgid to $hostid if non-zero.

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info SET
                        hostid=' . $dbc->Param('a') . '
	                WHERE mtgid=' . $dbc->Param('b'));

  return($dbc->Execute($sql, array($hostid, $mtgid)));
}

function update_meeting_location($mtgid, $locid, $locstatus)
{
  // Set the location of $mtgid to $locid with $locstatus.

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info SET
                        locationid=' . $dbc->Param('a') . ',
                        locationstatus=' . $dbc->Param('b') . '
	                WHERE mtgid=' . $dbc->Param('c'));

  return($dbc->Execute($sql, array($locid, $locstatus, $mtgid)));
}

function update_meeting_owner($mtgid, $contactid)
{
  // Set the owner of $mtgid to $contactid if non-zero.

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info SET
                        contactid=' . $dbc->Param('a') . '
	                WHERE mtgid=' . $dbc->Param('b'));

  return($dbc->Execute($sql, array($contactid, $mtgid)));
}

function update_meeting_potential_locations($mtgid, $locids, $locprefs)
{
  // Update $mtgid, setting $locids to $locprefs.

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $dbc->StartTrans();

  // We do this by tossing everything and reloading, but first clear
  // any previous location (including -1 or -2)

  update_meeting_location($mtgid, 0, "N");
  
  $sql = $dbc->Prepare('DELETE FROM mrsbs_potential_locations
	                WHERE mtgid=' . $dbc->Param('a'));

  if($dbc->Execute($sql, array($mtgid)))
  {
    for($i = 0;$i < count($locids);$i++)
    {
      $sql = $dbc->Prepare('INSERT INTO mrsbs_potential_locations
                            (locationid, mtgid, pref)
                            VALUES (' . $dbc->Param('a') . ',' .
	                                $dbc->Param('b') . ',' .
			                $dbc->Param('c') . ')');

      $dbc->Execute($sql, array($locids[$i], $mtgid, $locprefs[$i]));
    }
  }

  return($dbc->CompleteTrans());
}

function update_meeting_status($mtgid, $status)
{
  // Set the status of $mtgid to $status.  "X" is a special status
  // indicating the event has been canceled, and "x" indicates the
  // event has been uncanceled.

  // Returns: The previous status if successful, false otherwise.

  global $dbc;
  global $tx;
  
  $sql = $dbc->Prepare("SELECT status FROM mrsbs_meeting_info
	                WHERE mtgid=" . $dbc->Param('a'));
  
  $c = $dbc->GetOne($sql, array($mtgid));

  $sql = false;
  $sqla = false;

  if($status == "X")
  {
    if(ctype_upper($c))
    {
      record_history($mtgid, $_SESSION['contactid'], "CNCL", $tx['hi.cncl']);

      $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info SET
                            status=' . $dbc->Param('a') . '
	                    WHERE mtgid=' . $dbc->Param('b'));

      $sqla = array(strtolower($c), $mtgid);
    }
    else  // can't cancel a canceled meeting
      return(false);
  }
  elseif($status == "x")
  {
    if(ctype_lower($c))
    {
      record_history($mtgid, $_SESSION['contactid'], "UNCL", $tx['hi.uncl']);

      $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info SET
                            status=' . $dbc->Param('a') . '
	                    WHERE mtgid=' . $dbc->Param('b'));

      $sqla = array(strtoupper($c), $mtgid);
    }
    else  // can't reinstate an active meeting
      return(false);
  }
  else
  {
    $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info SET
                          status=' . $dbc->Param('a') . '
	                  WHERE mtgid=' . $dbc->Param('b'));

    $sqla = array($status, $mtgid);
  }

  if($dbc->Execute($sql, $sqla))
    return($c);
  else
    return(false);
}

function update_meeting_time($mtgid, $start)
{
  // Set the scheduled start time for $mtgid to $start.  If $start is -1,
  // set the start time to the most preferred begin time window.

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  if($start > -1)
  {
    $sql = 'UPDATE mrsbs_meeting_info SET
            scheduledfor=' . $dbc->DBTimeStamp($start) . '
	    WHERE mtgid=' . sstr($mtgid);

    return($dbc->Execute($sql));
  }
  else
  {
    // Limit isn't portable, so we can't do something like
    // 'UPDATE mrsbs_meeting_info SET
    //  scheduledfor=(SELECT begin FROM mrsbs_windows
    //                WHERE mtgid=' . $mtgid . '
    //                ORDER BY pref LIMIT 1)
    //  WHERE mtgid=' . $mtgid

    $sql = $dbc->Prepare('SELECT begin FROM mrsbs_windows
                          WHERE mtgid=' . $dbc->Param('a') . '
                          ORDER BY pref');
    
    $begin = $dbc->GetOne($sql, array($mtgid));
    
    $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info SET
                          scheduledfor=' . $dbc->Param('a') . '
	                  WHERE mtgid=' . $dbc->Param('b'));

    return($dbc->Execute($sql, array($begin, $mtgid)));
  }
}

function update_meeting_what($mtgid, $summary, $desc)
{
  // Update $mtgid with $summary and $desc.

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info SET
                        summary=' . $dbc->Param('a') . ',
                        description=' . $dbc->Param('b') . '
                        WHERE mtgid=' . $dbc->Param('c'));
      
  $records = $dbc->Execute($sql, array($summary, $desc, $mtgid));

  return($records ? true : false);
}

function update_meeting_when($mtgid, $windowids, $prefs, $froms, $untils,
			     $removes, $curprefs, $curfroms, $curuntils)
{
  // Update $windowids in $mtgid with $prefs, $froms, $untils, and
  // $removes, based on $curprefs, $curfroms, and $curuntils.

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $ret = true;

  for($i = 0;$i < count($windowids);$i++)
  {
    if($prefs[$i] != $curprefs[$i])
    {
      // Update this preference

      $sql = $dbc->Prepare('UPDATE mrsbs_windows SET
                            pref=' . $dbc->Param('a') . '
	                    WHERE windowid=' . $dbc->Param('b'));

      if(!$dbc->Execute($sql, array($prefs[$i], $windowids[$i])))
	$ret = false;
    }
    
    if($froms[$i] != $curfroms[$i])
    {
      // Update this from time

      $sql = 'UPDATE mrsbs_windows SET
              begin=' . $dbc->DBTimeStamp($froms[$i]) . '
	      WHERE windowid=' . sstr($windowids[$i]);

      if(!$dbc->Execute($sql))
	$ret = false;
    }
    
    if($untils[$i] != $curuntils[$i])
    {
      // Update this until time

      $sql = 'UPDATE mrsbs_windows SET
              end=' . $dbc->DBTimeStamp($untils[$i]) . '
	      WHERE windowid=' . sstr($windowids[$i]);

      if(!$dbc->Execute($sql))
	$ret = false;
    }
  }

  for($i = 0;$i < count($removes);$i++)
  {
    // Remove this windowid

    $sql = $dbc->Prepare('DELETE FROM mrsbs_windows
	                  WHERE windowid=' . $dbc->Param('a'));

    if(!$dbc->Execute($sql, array($removes[$i])))
      $ret = false;
  }

  if(count($removes) > 0)
  {
    // We need to update the preferences, which are still in the correct
    // order but have numbering that won't make sense to the interface.
    // If, eg, we have three windows with prefs 2,2,4 after removal, we
    // need them to be 1,1,2 instead (1,1,3 would also be OK, but is
    // harder to implement).

    // Get remaining windows, in order
    
    $sql = $dbc->Prepare("SELECT windowid, pref FROM mrsbs_windows
                          WHERE mtgid=" . $dbc->Param('a') .
			 " ORDER BY PREF ASC");

    $records = $dbc->Execute($sql, array($mtgid));

    if($records)
    {
      $pref = 0;
      $last = -1;
      
      while(!$records->EOF)
      {
	if($records->fields['pref'] != $last)
	  $pref++;
	
	$sql = $dbc->Prepare("UPDATE mrsbs_windows SET pref="
			     . $dbc->Param('a') .
			     " WHERE windowid=" . $dbc->Param('b'));

	$dbc->Execute($sql, array($pref, $records->fields['windowid']));

	$last = $records->fields['pref'];
	$records->MoveNext();
      }
    }
  }
  
  if($ret)
  {
    // If the meeting previously failed to schedule, reset the meeting status

    $sql = $dbc->Prepare("UPDATE mrsbs_meeting_info SET status='I'
                          WHERE mtgid=" . $dbc->Param('a') .
			  " AND status='F'");

    $dbc->Execute($sql, $mtgid);
  }
  
  return($ret);
}

function update_meeting_when2($mtgid, $duration, $replyby, $replybymode,
			      $curduration, $curreplyby, $curreplybymode)
{
  // Update $mtgid with $duration, $replyby, and $replybymode, based
  // on $curduration, $curreplyby, and $curreplybymode.

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $ret = true;

  if($duration != $curduration)
  {
    // Update duration

    $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info SET
                          duration=' . $dbc->Param('a') . '
	                  WHERE mtgid=' . $dbc->Param('b'));

    if(!$dbc->Execute($sql, array($duration, $mtgid)))
      $ret = false;
  }

  if($replyby != $curreplyby)
  {
    // Update reply by time
    
    $sql = 'UPDATE mrsbs_meeting_info SET
            replyby=' . $dbc->DBTimeStamp($replyby) . '
	    WHERE mtgid=' . sstr($mtgid);

    if(!$dbc->Execute($sql))
      $ret = false;
  }
   
  if($replybymode != $curreplybymode)
  {
    // Update reply by mode

    $sql = $dbc->Prepare('UPDATE mrsbs_meeting_info SET
                          replybymode=' . $dbc->Param('a') . '
	                  WHERE mtgid=' . $dbc->Param('b'));

    if(!$dbc->Execute($sql, array($replybymode, $mtgid)))
      $ret = false;
  }

  if($ret)
  {
    // If the meeting previously failed to schedule, reset the meeting status

    $sql = $dbc->Prepare("UPDATE mrsbs_meeting_info SET status='I'
                          WHERE mtgid=" . $dbc->Param('a') .
			  " AND status='F'");

    $dbc->Execute($sql, $mtgid);
  }
   
  return($ret);
}

function update_meeting_who($mtgid, $inviteids, $statuses, $removes,
			    $curstatuses)
{
  // Update $inviteids in $mtgid with $statuses and $removes, based on
  // $curstatuses.

  // Returns: true if fully successful, false otherwise.

  global $dbc;

  $ret = true;

  for($i = 0;$i < count($inviteids);$i++)
  {
    if($statuses[$i] != $curstatuses[$i])
    {
      // Update this inviteid

      $sql = $dbc->Prepare('UPDATE mrsbs_invitees SET
                            status=' . $dbc->Param('a') . '
	                    WHERE inviteid=' . $dbc->Param('b'));

      if(!$dbc->Execute($sql, array($statuses[$i], $inviteids[$i])))
	$ret = false;
    }
  }

  for($i = 0;$i < count($removes);$i++)
  {
    // Remove this inviteid

    $sql = $dbc->Prepare('DELETE FROM mrsbs_invitees
	                  WHERE inviteid=' . $dbc->Param('a'));

    if(!$dbc->Execute($sql, array($removes[$i])))
      $ret = false;
  }

  return($ret);
}

function update_recent_invitees($contactid, $inviteecontactid)
{
  // Update the list of recent invitees for $contactid, with $inviteecontactid
  // as the most recent invitee.

  // Returns: true if fully successful, false otherwise.

  global $dbc, $config;

  // We don't validate $contactid or $inviteecontactid, but perhaps we should
  
  $dbc->StartTrans();

  $cur = get_recent_invitees($contactid, "lastinvite");

  if($cur)
  {
    // First see if $inviteecontactid is already in the table

    $found = 0;

    for($i = 0;$i < $cur['count'];$i++)
    {
      if($cur[$i]['contactid'] == $inviteecontactid)
      {
	$found++;
	break;
      }
    }

    if($found)
    {
      // Just update the last time
      
      $sql = $dbc->Prepare('UPDATE mrsbs_recent_invitees
                            SET lastinvite=' . $dbc->DBTimeStamp(time()) . '
                            WHERE contactid=' . $dbc->Param('a') . '
                            AND inviteeid=' . $dbc->Param('b'));

      $dbc->Execute($sql, array($contactid, $inviteecontactid));
    }
    else
    {
      // Insert the new record

      $sql = $dbc->Prepare('INSERT INTO mrsbs_recent_invitees
                            (contactid, inviteeid, lastinvite)
                            VALUES (' . $dbc->Param('a') . ',' .
			            $dbc->Param('b') . ',' .
			            $dbc->DBTimeStamp(time()) . ')');

      $dbc->Execute($sql, array($contactid, $inviteecontactid));

      // Drop the oldest record(s) if we have too many.  >= in case $config
      // has changed since last we checked.
	
      if($cur['count'] >= $config['maxrecentinvitees'])
      {
	// Keep in mind we've already added a row, so <= adjusts for that.
	
	for($i = 0;$i <= ($cur['count'] - $config['maxrecentinvitees']);$i++)
	{
	  $sql = $dbc->Prepare('DELETE FROM mrsbs_recent_invitees
                                WHERE contactid=' . $dbc->Param('a') . '
                                AND inviteeid=' . $dbc->Param('b'));

	  $dbc->Execute($sql, array($contactid, $cur[$i]['contactid']));
	}
      }
    }
  }
  else
    $dbc->FailTrans();
  
  return($dbc->CompleteTrans());
}

?>
Return current item: Meeting Request Scheduling and Booking System