#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <ctype.h>

#include "../global.h"
#include "../survey.h"
#include "../custom.h"
#include "shared_util.h"        /* for cs_critical_error(), etc */
#include "shared_cs_util.h"     /* for cs_critical_error(), etc */
#include "shared_file_util.h"   /* for file_exists() */
#include "shared_msg_line.h"    /* for remove_smart_quotes() */
#include "shared_lock.h"

/*** argh!!!! global variables ... sheesh! ***/
char discussion_path[MAX_PATH + 1];	        /* eg: ../courses/sample/people/username/discuss/handouts */
char central_discussion_path[MAX_PATH + 1];     /* eg. ../courses/sample/inboxes/handouts */
char discussion_fname[MAX_FILENAME + 1];	/* eg, "discuss" , "anon", etc. used to name the directory */


/** heads of linked lists used in this module */
MSG_TREE_NODE *tree_head = 0;
USER_INBOX_NODE   *user_head = 0;



/* What is x in <textarea rows=.. cols=x> used to edit Manhattan messages?
** All values are #defined in custom.h
** If ENABLE_EDITOR_WIDTH_SELECTION is non-zero, teachers will be able to
** select from one of 4 pre-defined editor widths
*/

int editor_width_config(int config_setting)
{

 if(!ENABLE_EDITOR_WIDTH_SELECTION)
      return EDITOR_WIDTH_STANDARD;

 switch(EDITOR_WIDTH_PREFERENCE(config_setting)) 
  {
   case 0: return EDITOR_WIDTH_STANDARD; break;
   case 1: return EDITOR_WIDTH_MEDIUM; break;
   case 2: return EDITOR_WIDTH_LARGE; break;
   case 3: return EDITOR_WIDTH_XLARGE; break;
   default: return EDITOR_WIDTH_STANDARD; 
  } 
  
}







/* can only faculty post messages to grp
** call this function to determine whether
** or not to put a "new item" button on the
** screen when accessed by a student
 */
int
faculty_only_group (int grp)
{
  return grp == HANDOUTS_NOTICES ||
    grp == LECTURES || grp == SELFTEST || grp == INTERNET_RESOURCES;
}


/* Only the modules listed below are organized by topic
*/
int
group_uses_topic (int grp)
{
  return (grp == CLASS_DISCUSSION) ||
    (grp == STUDENT_LOUNGE) ||
    (grp == ANONYMOUS_DISCUSSION) ||
    (grp == TEAM_DISCUSSION) || (grp == TEAM_TEACH) || (grp == ASSIGNMENTS);
}



/* a 'name_filter' is the drop-down selection list
** listing the names of all of the people in the 
** class.  Call this function to determine whether
** or not to show this list for a particular grp
*/

int
group_uses_name_filter (int grp)
{
  return (grp == CLASS_DISCUSSION) ||
    (grp == STUDENT_LOUNGE) ||
    (grp == TEAM_DISCUSSION) || (grp == TEAM_TEACH);
}



/* Certain global variables are used throughout the programs
** to record things like the name of the module and its 
** path.  Call this function to set those variables!
*/

void
set_discussion_names (const char *coursepath,  const char *username, int grp)
{
  switch (grp)
    {
    case CLASS_DISCUSSION:
      strncpy (discussion_fname, DISCUSSION_DIR, MAX_FILENAME + 1);
      break;

    case STUDENT_LOUNGE:
      strncpy (discussion_fname, LOUNGE_DIR, MAX_FILENAME + 1);
      break;


    case ANONYMOUS_DISCUSSION:
      strncpy (discussion_fname, ANONYMOUS_DIR, MAX_FILENAME + 1);
      break;

    case TEAM_DISCUSSION:
      strncpy (discussion_fname, TEAM_DIR, MAX_FILENAME + 1);
      break;


    case HANDOUTS_NOTICES:
      strncpy (discussion_fname, HANDOUTS_DIR, MAX_FILENAME + 1);
      break;

    case INTERNET_RESOURCES:
      strncpy (discussion_fname, INET_DIR, MAX_FILENAME + 1);
      break;


    case ASSIGNMENTS:
      strncpy (discussion_fname, ASSIGNMENTS_DIR, MAX_FILENAME + 1);
      break;


    case TEAM_TEACH:
      strncpy (discussion_fname, TEAM_TEACH_DIR, MAX_FILENAME + 1);
      break;

    case GRADES:
      strncpy (discussion_fname, GRADES_DIR, MAX_FILENAME + 1);
      break;


    case SELFTEST:
      strncpy (discussion_fname, SELFTEST_DIR, MAX_FILENAME + 1);
      break;


    case MAIL:
      strncpy (discussion_fname, MAIL_DIR, MAX_FILENAME + 1);
      break;


    case LECTURES:
      strncpy (discussion_fname, LECTURES_DIR, MAX_FILENAME + 1);
      break;

    case SURVEYS:
      strncpy (discussion_fname, SURVEYS_DIR, MAX_FILENAME + 1);
      break;



    case CHATTER:
      strncpy (discussion_fname, CHAT_DIR, MAX_FILENAME + 1);
      break;

    case PODCASTS:
      strncpy (discussion_fname, PODCASTS_DIR, MAX_FILENAME + 1);
      break;

    case CLIPBOARD:
      strncpy (discussion_fname, "", MAX_FILENAME + 1);
      break;

    default:
      cs_critical_error (ERR_UNKNOWN_CASE_IN_SWITCH, "");
      break;
    }
  snprintf (discussion_path, MAX_PATH + 1, "%s%s/%s/%s/", coursepath, PEOPLE_DIR, username, discussion_fname);

  snprintf (central_discussion_path, MAX_PATH + 1, "%s%s/%s/", coursepath, INBOXES_DIR, discussion_fname);


}


/* Searches the message tree, starting at 'root', for
** the message identified by msg_id
*/

static
MSG_TREE_NODE * find_message(int msg_id, MSG_TREE_NODE *root)
{	
 MSG_TREE_NODE *ptr;
 
  if(!root)
     return (MSG_TREE_NODE *) 0;
  if(root->data.info.msg_id == msg_id)
	  return root;
  ptr = find_message(msg_id,root->next);
  if(!ptr)
    ptr = find_message(msg_id, root->reply);
  return ptr;
}


/* returns a MSG_TREE_NODE ptr to a newly allocated
** node on the tree.  We must search the tree for
** the correct insertion point for this new message.
** We know that the message is in reply to reply_to_id,
** and it is part of the topic topic_id
*/
static
MSG_TREE_NODE *new_normal_msg(int reply_to_id, int topic_id)
{
 MSG_TREE_NODE *ptr, *topic_ptr;
 
 /* first, find the topic associated with this message */
 for(ptr = tree_head; ptr && ptr->data.info.msg_id != topic_id; ptr=ptr->next);
 
 /* The above search can fail if the topic hasn't been inserted
 ** into the tree.  This can happen, for example, if the teacher
 ** has 'hidden' or 'scheduled' a topic
 ** If the search does  fail, we simply return a null pointer
 */
 if(!ptr) 
	 return (MSG_TREE_NODE *) 0;

/* If the message is not a direct reply to the topic
** we just found, we must call find_message() to find the
** correct message.
*/
topic_ptr = ptr;    /* save a pointer to the topic */
 if(ptr->user_data.msg_id != reply_to_id)
 {
  ptr = find_message(reply_to_id, ptr->reply);
  /* if find_message() fails, it means the msg this message is
  ** a reply_to has not been inserted in the tree.  This can happen
  ** for example, in certain circumstances regarding Hot Potatoes tests
  ** in the Assignments module.
  ** Instead of not inserting the message at all, we'll insert the message
  ** as a direct reply to the topic
  **/
  if(!ptr)
      ptr = topic_ptr;
 }
 
 /* ptr now points to the message the new message is a 
 ** direct reply to.  This message needs to go at the
 ** end of the reply chain
 */
 if(ptr->reply)
   {
    for(ptr=ptr->reply;ptr->next; ptr = ptr->next);
    ptr->next = (MSG_TREE_NODE *) malloc(sizeof(MSG_TREE_NODE));
    if(!ptr->next)
      cs_critical_error(ERR_MALLOC_FAILED,"");
     ptr->next->prev = ptr;
     ptr = ptr->next;
   }
  else
   {
     ptr->reply = (MSG_TREE_NODE *) malloc(sizeof(MSG_TREE_NODE));
     if(!ptr->reply)
       cs_critical_error(ERR_MALLOC_FAILED, "");
     ptr->reply->prev = ptr;
     ptr = ptr->reply;
   }  
 return ptr;
}



/* returns a MSG_TREE_NODE ptr to a newly
** allocated node, that has been added
** to the tree.  Note that the root of the
** tree is a topic, as is tree_head->next,
** and tree_head->next->next, etc.
*/
 
static
MSG_TREE_NODE  *new_topic()
{
 MSG_TREE_NODE *ptr;
 
 if(!tree_head)
 {	 
  tree_head = (MSG_TREE_NODE *) malloc (sizeof(MSG_TREE_NODE));
  if(!tree_head)
	  cs_critical_error(ERR_MALLOC_FAILED, "");
  tree_head->prev = (MSG_TREE_NODE *) 0;
  tree_head->next = (MSG_TREE_NODE *) 0;
  return tree_head;
 }
 else
  {
   for(ptr=tree_head; ptr->next; ptr = ptr->next);
   ptr->next = (MSG_TREE_NODE *) malloc (sizeof(MSG_TREE_NODE));
   if(!ptr->next)
	  cs_critical_error(ERR_MALLOC_FAILED, "");
   ptr->next->prev = ptr;
   ptr->next->next = (MSG_TREE_NODE *) 0;
   return ptr->next;
  }
} 




/* stores the message, represented by 'data' as 
** a new node in the message tree
*/ 
static
MSG_TREE_NODE *store_in_msg_tree(CENTRAL_INBOX_RECORD *data, long offset)
{
 MSG_TREE_NODE *ptr;
 USER_INBOX_NODE *user_ptr;
 
 if(data->info.reply_to == -1)  /* -1 means the msg is a topic */
   ptr = new_topic();
 else
 {	 
  ptr = new_normal_msg(data->info.reply_to, data->info.topic_id);
  if(!ptr)
    return 0;    /* can't place this item */
 }   
  ptr->data = *data;
  ptr->next = (MSG_TREE_NODE *) 0;
  ptr->reply = (MSG_TREE_NODE *) 0;
  ptr->offset = offset;
  for(user_ptr = user_head; 
        user_ptr && user_ptr->data.msg_id != data->info.msg_id;
		        user_ptr=user_ptr->next);
  if(user_ptr)
     ptr->user_data = user_ptr->data;
  else
   {
    ptr->user_data.msg_id = data->info.msg_id;
    ptr->user_data.time_read = 0;
    ptr->user_data.user_unlocked = 0;
    ptr->user_data.user_hidden = 0;
   } 
  return ptr;
}   
  




/** This is a support function for open_user_inbox()  (see next)
***
*** It converts from an old USER_INBOX_RECORD to a new one that 
*** was put into use starting with Manhattan 3.2
***
**/

static void
convert_old_user_inbox()
{
 typedef struct old_user_inbox_struct {
 	
   int msg_id;	                   /* which message does this record refer to? */
   time_t time_read;      	   /* time this user first read this message */
   unsigned char user_unlocked;     /* did the user 'unlock' the attachments */
   unsigned char  user_hidden;     /* did this user choose to hide this msg? */
 } OLD_USER_INBOX_RECORD;

 FILE *in_fp, *out_fp;
 OLD_USER_INBOX_RECORD old_record;
 USER_INBOX_RECORD new_record;
 char oldpath[MAX_PATH + 1];
 char newpath[MAX_PATH + 1];
 
 /* in the old version, the file was called INBOX_FNAME (or inbox.dat) */
 snprintf(oldpath, MAX_PATH + 1, "%s%s", discussion_path, INBOX_FNAME);
 in_fp = fopen(oldpath, "r");
 if(!in_fp)
      return;

   
/* in the new version, the file is called USER_INBOX_FNAME (or user_inbox.dat) */    
 snprintf(newpath, MAX_PATH + 1, "%s%s", discussion_path, USER_INBOX_FNAME);   
 out_fp = fopen(newpath, "w");

/* reporting critical_error on the failure of above open caused problems with Podcast
** module, which sometimes calls this with user __unknown__
**/
 if(!out_fp)
   return;
   // cs_critical_error(ERR_FOPEN_WRITE_FAILED, newpath);

 if(in_fp)
  {
   while(fread(&old_record, sizeof(old_record), 1, in_fp) == 1)
    {
     new_record.msg_id = old_record.msg_id;
     new_record.time_read = old_record.time_read;
     new_record.user_unlocked = old_record.user_unlocked;
     new_record.user_hidden = old_record.user_hidden;
     new_record.flagged = 0;
     new_record.future1 = 0;
     new_record.future2 = 0;

     if(fwrite(&new_record, sizeof(new_record), 1, out_fp) != 1)
         cs_critical_error(ERR_FWRITE_FAILED, newpath);
    }
   fclose(in_fp);
  } 
 fclose(out_fp);
 unlink(oldpath);
}

   
/*** The data structure 
****  USER_INBOX_RECORD
**** (see global.h) changed at Manhattan version 3.2
****
**** A new data item 
****
****   unsigned char flagged
****
**** was added to the struct to record whether or not
**** the user marked the message with a 'flag'.  At the same
**** time two additional unsigned chars were added for future
**** use.
****
**** These records are written to disk as an "inbox.dat" file in
**** version prior to 3.2  Instead of writing a conversion program
**** that would convert all of these files from the 'old' to the 'new'
*** version of USER_INBOX_RECORD, it was decided to handle the translations
*** on the fly.
***
*** The following function MUST BE USED whenever a program needs to open
*** the file containing records of type USER_INBOX_RECORD.
***
*** THE FUNCTION ASSUMES set_discussion_names() HAS BEEN CALLED.
***
*** This function will convert from the old record format to the new as required.
*** unless SUPPORT_PRE32_USER_INBOXES is #defined in custom.h and is set to 0
**/ 
 
 
FILE *open_user_inbox(const char *mode)
{
 FILE *fp;
 char path[MAX_PATH + 1];
 char oldpath[MAX_PATH + 1];
 
 snprintf(path, MAX_PATH + 1, "%s%s", discussion_path, USER_INBOX_FNAME);
 
/* forget the conversion nonsense if SUPPORT_PRE32_USER_INBOXES is 
** #defined in custom.h and is set to '0'
*/
 
#ifdef SUPPORT_PRE32_USER_INBOXES
 if(!SUPPORT_PRE32_USER_INBOXES)
   {
    fp = fopen(path, mode);
    return fp;
   } 
#endif 
 
 if(!strcmp(mode, "r") || !strcmp(mode, "r+"))
  {
    fp = fopen(path, mode);
    if(!fp)
     {
      convert_old_user_inbox();
      fp = fopen(path, mode);
     }
    return fp;
  } 
 else if(!strcmp(mode, "w") || !strcmp(mode, "w+"))
  {
   snprintf(oldpath, MAX_PATH + 1, "%s%s", discussion_path, INBOX_FNAME);
   unlink(oldpath);
   fp = fopen(path, mode);
   return fp;
  }
 else if(!strcmp(mode, "a") || !strcmp(mode, "a+"))
  {
   if(!file_exists(path))   /* shared_file_util.c */
         convert_old_user_inbox();
   fp = fopen(path, mode);
   return fp;
  }
 else
  {
   cs_critical_error(ERR_GENERAL_ERROR, "Unsupported mode in open_user_inbox()"); 
   return (FILE *) 0;
  } 

} 
 
 
 
 
 
/** no file locking required, since we assume only this 
** user will have access to his personal user_inbox_record
**/
void 
write_user_inbox_record(int msg_id, time_t time_read)
{
 USER_INBOX_RECORD record;
 FILE *fp;
 
 record.msg_id = msg_id;
 record.time_read = time_read;
 record.user_hidden = 0;
 record.user_unlocked = 0;
 record.flagged = 0; 
 record.future1 = 0;
 record.future2 = 0;

 

 fp = open_user_inbox("a");     /* shared_news_util.c */ 
 if(!fp)
   cs_critical_error(ERR_FOPEN_WRITE_FAILED,USER_INBOX_FNAME);
 if(fwrite(&record,sizeof(USER_INBOX_RECORD),1,fp) != 1)
   cs_critical_error(ERR_FWRITE_FAILED, USER_INBOX_FNAME);
 fclose(fp);
}   

 
 
 

/* ASSUMES set_discussion_names() has already been called
** Builds a linked list of the records stored in the
** user's LOCAL inbox.dat file
**/
USER_INBOX_NODE * build_user_inbox_list()
{
 FILE *fp;
 USER_INBOX_RECORD data;
 USER_INBOX_NODE *head = 0;
 USER_INBOX_NODE *ptr = 0;
 
 
 fp = open_user_inbox("r");   
 if(!fp)                              /* inbox.dat doesn't exist */
	 return (USER_INBOX_NODE *) 0;  /* so there is no list */
 
 /*** we don't have to lock the personal inbox.dat file, since only one user
  * can ever access it.  Is this OK?
  */
 
 while(fread( &data, sizeof(USER_INBOX_RECORD),1,fp) == 1)
 {
  if(!head)
   { 
    head = (USER_INBOX_NODE *)malloc(sizeof(USER_INBOX_NODE));
    if(!head)
      cs_critical_error(ERR_MALLOC_FAILED, "");
    head->data = data;
    head->next = 0;
    ptr = head;
   }
  else
   {
    ptr->next = (USER_INBOX_NODE *) malloc(sizeof(USER_INBOX_NODE));
    if(!ptr->next)
          cs_critical_error(ERR_MALLOC_FAILED, "");
    ptr = ptr->next;
    ptr->data = data;
    ptr->next = 0;
   }
 }

 /** nothing to unlock???  */
 fclose(fp);
 return head;
} 


void
free_user_inbox_list()
{
 USER_INBOX_NODE *ptr;	
 
 while(user_head)
 {
   ptr=user_head;
   user_head = user_head->next;
   free(ptr);
 }
} 



int
is_teacher_hidden (MSG_STATUS status, time_t release_time, time_t unrelease_time, time_t current_time)
{
 MSG_STATUS basic_status;
 
 basic_status = BASIC_STATUS(status);  /* BASIC_STATUS macro #defined in global.h */
 
 return		 ((basic_status == TEACHER_HIDDEN) ||
		  ((basic_status == TIMED_RELEASE) && 
	                ((release_time > current_time) || 
                            (unrelease_time && (unrelease_time < current_time))  )) ); 

}


/* see notes where this is called below */
static
int is_one_team_assignment(CENTRAL_INBOX_RECORD *data, char *target_team)
{


 if( (data->info.reply_to == -1) &&
       (strlen(data -> info.subject) > 3) &&
      (*(data->info.subject) == '|')     &&
      (*(data->info.subject + 2) == '|') )
   
   {
     *target_team = *(data->info.subject + 1);
     if( ((*target_team >= 'A') && (*target_team <= 'Z')) ||
         ((*target_team >= 'a') && (*target_team <= 'z')) )   /* problem - but teachers know these as A-A ... Z-Z */
           return 1;
    else
           return 0;
   }
 else
    return 0;
    
}                     





/* should the message, represented by 'data'
** be seen by the user represented by 'user'?
*/
int msg_should_display(CENTRAL_INBOX_RECORD *data, 
                            SESSION *user, int grp, 
                             time_t current_time, int fac_teamteach, int show_teacher_hidden)
{

 char target_team;
 int passed_team_check;


 
 /* no one sees DELETED msgs */	
 if(BASIC_STATUS(data->status) == DELETED)   /* teacher deleted this message */
    return 0;

/* handle teacher_hidden msgs.  Note that the teacher will never be blocked from seeing a teacher-hidden msg
** that is not a topic
*/ 
 if(is_teacher_hidden (data->status, data->release_time, data->unrelease_time, current_time))
  {
    if(user->group == STUDENT)  /* students never see teacher hidden msgs */
       return 0;
    if( (user->group == FACULTY)  && (data->info.reply_to == -1) && !show_teacher_hidden)
        return 0;  /* teachers don't see teacher hidden TOPICS if the parameter says 'no' */
  }    
 
 

  /* if we are in the TEAM_DISCUSSION, show this msg only if
   * it is for the proper team
   */
 if(grp == TEAM_DISCUSSION)
    return (user->team == data->info.sender_team);
    
        
 if(grp == TEAM_TEACH)
 {
   if(fac_teamteach)  /* special case - the user is FACULTY and needs a full list of msgs for all teams */
        return 1;
   else
	return (user->team == data->info.sender_team);   
 }


/* Assignments module - 
** A teacher absolutely had to have a way to deliver an assignment (an exam actually) only to 
** members of a team.  That is, he wanted one exam to go to team A, another version of the exam
** to team B, and so on.
**
** To accomplish this in a kludgy way, when an assigment is posted to the assignments module, 
** a teacher can direct that assignment only to a certain team by starting the Subject: of the
** assigment with:
**
**   |<team letter|
** 
** eg.
**
** An assignment with the subject:
** 
**     |A| Your first exam
**
** will be seen only by team A
*/

 if(grp == ASSIGNMENTS)
  {
    if(user->group == FACULTY)
	return 1;
    else
     {
        if(is_one_team_assignment(data,&target_team))
           passed_team_check = (target_team == user->team);
        else
           passed_team_check = 1;
       return passed_team_check && (!*(data->info.receiver_username) ||        
                     !strcmp(data->info.receiver_username, user->username));
     }
  }    		  
 
  
 /*** all other modules, everyone sees everything ****/
  return 1;
 }  




static int
topic_cmp (CENTRAL_INBOX_RECORD * a, CENTRAL_INBOX_RECORD * b, TOPIC_SORT_NODE *sort_head)
{
  TOPIC_SORT_NODE *ptr;
  
  
  if(a->info.msg_id == b->info.msg_id)
     return 0;
     
  for(ptr = sort_head; ptr; ptr = ptr->next)
   {
    if(a->info.msg_id == ptr->msg_id)
       return -1;
    if(b->info.msg_id == ptr->msg_id)
       return +1;
   }
   
  if(a->info.time_sent < b->info.time_sent)
     return -1;
  if(a->info.time_sent > b->info.time_sent)
     return +1;
     
  return 0;
}

                


static 
TOPIC_SORT_NODE * build_topic_sort_list(char team, int grp)
{
 FILE *fp;
 int fd;
 int msg_id;
 char sort_file[MAX_PATH + 1];
 TOPIC_SORT_NODE *head = 0;
 TOPIC_SORT_NODE *ptr;
 char teamstring[5];
 
 
 snprintf(sort_file,MAX_PATH + 1, "%s%s", central_discussion_path,TOPIC_SORT_FNAME);
 
 
 if((grp == TEAM_DISCUSSION) || (grp == TEAM_TEACH))
  {
   snprintf(teamstring,5,".%c", team);
   strcat(sort_file, teamstring);
  }
   
 fp = fopen(sort_file, "r");
 if(!fp)
   return (TOPIC_SORT_NODE *) 0;

 fd = fileno(fp);
 get_shared_lock(fd,1);   /* shared_lock.c */   

 while(fread(&msg_id, sizeof(int),1,fp) == 1)
  {
   if(!head)
    {
     head = (TOPIC_SORT_NODE *)malloc(sizeof(TOPIC_SORT_NODE));
     if(!head)
        cs_critical_error(ERR_MALLOC_FAILED, "");
     ptr = head;
    }
    else
     {     
      for(ptr=head; ptr->next; ptr=ptr->next);
      
      ptr -> next = (TOPIC_SORT_NODE *)malloc(sizeof(TOPIC_SORT_NODE));
      if(!ptr->next)
        cs_critical_error(ERR_MALLOC_FAILED, "");
      ptr = ptr->next;
     }
     
     ptr->msg_id = msg_id;
     ptr->next = (TOPIC_SORT_NODE *) 0;
   }
   
 release_lock(fd);
 fclose(fp);
 
 return head;
}

     
static
void free_topic_sort_list(TOPIC_SORT_NODE *head)
{
 TOPIC_SORT_NODE *ptr;
 
 while(head)
  {
    ptr = head;
    head = head->next;
    free(ptr);
  }
}



static void
adjust_previous_msg_tree_ptrs(void)
{
 MSG_TREE_NODE *ptr;
 
 if(!tree_head)
    return;
    
 tree_head -> prev = (MSG_TREE_NODE *) 0;
    
 for(ptr=tree_head; ptr; ptr=ptr->next)
  {
    if(ptr->next)
      ptr->next->prev = ptr;
  }
}


  
 
           
static void
sort_msg_tree (char team, int grp)
{
  TOPIC_SORT_NODE *sort_head;
  MSG_TREE_NODE *q, *tail, *p, *r;
  
  sort_head = build_topic_sort_list(team, grp);
  
  if (tree_head != NULL)
    {
      tail = tree_head;
      while (tail->next)
	{
	  q = tail->next;
	  if (topic_cmp (&(q->data), &(tree_head->data),sort_head) < 0)
	    {
	      tail->next = q->next;
	      q->next = tree_head;
	      tree_head = q;
	    }
	  else
	    {
	      r = tree_head;
	      p = r->next;
	      while (topic_cmp (&(q->data), &(p->data), sort_head) > 0)
		{
		  r = p;
		  p = r->next;
		}
	      if (topic_cmp(&(q->data),  &(p->data), sort_head)== 0)
		tail = q;
	      else
		{
		  tail->next = q->next;
		  q->next = p;
		  r->next = q;
		}
	    }
	}
    }
 free_topic_sort_list(sort_head);
 adjust_previous_msg_tree_ptrs();
}







/* assumes set_discussion_names() has already been called!
**
** This function reads the central inbox file and builds an unordered
** doubly-linked binary tree in memory with global tree_head
** pointing to the root.
**
** Each node (of type MSG_TREE_NODE) in the tree represents one 
** message for the discussion module 'grp'
**
** The root (tree_head) of the tree is the first topic.
** From tree_head, following the "next" branches leads to the
** other topics.  The "reply" branches from these initial
** topic nodes are the first direct replies to the original topic.
**
** For all other msgs, the "next" branch points to the next msg
** that is a reply to the same message that the current msg
** is a reply to.  The "reply" branch points to the first msg
** that is a direct reply to this msg.
** 
** We are building this user's view of the msg tree.  In order
** to do that, we need to first build a second linked list
** which represents the user's particular preferences, read
** from their local inbox file.
**
** Complicated, yes.
*/
void build_msg_tree(SESSION *user, int grp, int show_all_teamteach, int show_teacher_hidden, time_t *current_time)
{
 int fd;
 FILE *fp;
 char inbox_fname[MAX_PATH + 1];  /* full path to central inbox */
 CENTRAL_INBOX_RECORD data;
 int long offset = 0;
 int record_size;
 
 record_size = sizeof(CENTRAL_INBOX_RECORD);

 *current_time = time(NULL); /* since some msgs might be timed released */
 
 user_head = 0;     /* these globals should already be set, but just in case */
 tree_head = 0;
 
 user_head = build_user_inbox_list();  
 
 snprintf(inbox_fname,MAX_PATH + 1,"%s%s",central_discussion_path,INBOX_FNAME);
 fp = fopen(inbox_fname,"r");
 if(!fp)                              /* inbox.dat doesn't exist */
  {
    free_user_inbox_list();	  
    return;  /* so there is no list */
  } 
 
 fd = fileno(fp);
 get_shared_lock(fd, 1);                      /* shared_lock.c */

while(fread(&data,record_size,1,fp) == 1)
{
 if(msg_should_display(&data,user,grp,*current_time,show_all_teamteach, show_teacher_hidden))
      store_in_msg_tree(&data, offset);  /* note we are IGNORING failure here! */
 offset += record_size;
} 
		

 release_lock(fd);   /* shared_lock.c */
 fclose(fp);

 free_user_inbox_list();
 sort_msg_tree(user->team, grp);
 
} 



/*** this function is similar to msg_should_display() except
**** it always shows teacher-hidden messages
**** It's used for the 'event list' in the statistics programs
***/
static
int event_list_msg_should_display(CENTRAL_INBOX_RECORD *data, 
                            SESSION *user, int grp, 
                             time_t current_time, int fac_teamteach)
{

 
 /* no one sees DELETED msgs */	
 if(BASIC_STATUS(data->status) == DELETED)   /* teacher deleted this message */
    return 0;


  /* if we are in the TEAM_DISCUSSION, show this msg only if
   * it is for the proper team
   */
 if(grp == TEAM_DISCUSSION)
    return (user->team == data->info.sender_team);
    
        
 if(grp == TEAM_TEACH)
 {
   if(fac_teamteach)  /* special case - the user is FACULTY and needs a full list of msgs for all teams */
        return 1;
   else
	return (user->team == data->info.sender_team);   
 }

 if(grp == ASSIGNMENTS)
  {
    if(user->group == FACULTY)
	return 1;
    else
       return (!*(data->info.receiver_username) ||
	          !strcmp(data->info.receiver_username, user->username));
  } 		  
  
 /*** all other modules, everyone sees everything ****/
  return 1;
 }  



/*** this is similar to build_msg_tree()
**** except that it includes even teacher-hidden msgs
*** in the tree 
*** it's used by the stats_ind_calendar program
***/


void build_event_list_msg_tree(SESSION *user, int grp, int show_all_teamteach, time_t *current_time)
{
 int fd;
 FILE *fp;
 char inbox_fname[MAX_PATH + 1];  /* full path to central inbox */
 CENTRAL_INBOX_RECORD data;
 int long offset = 0;
 int record_size;
 
 record_size = sizeof(CENTRAL_INBOX_RECORD);

 *current_time = time(NULL); /* since some msgs might be timed released */
 
 user_head = 0;     /* these globals should already be set, but just in case */
 tree_head = 0;
 
 user_head = build_user_inbox_list();  
 
 snprintf(inbox_fname,MAX_PATH + 1,"%s%s",central_discussion_path,INBOX_FNAME);
 fp = fopen(inbox_fname,"r");
 if(!fp)                              /* inbox.dat doesn't exist */
  {
    free_user_inbox_list();	  
    return;  /* so there is no list */
  } 
 
 fd = fileno(fp);
 get_shared_lock(fd, 1);                      /* shared_lock.c */

while(fread(&data,record_size,1,fp) == 1)
{
 if(event_list_msg_should_display(&data,user,grp,*current_time,show_all_teamteach))
      store_in_msg_tree(&data, offset);  /* note we are IGNORING failure here! */
 offset += record_size;
} 
		

 release_lock(fd);   /* shared_lock.c */
 fclose(fp);

 free_user_inbox_list();
 sort_msg_tree(user->team, grp);
 
} 




/* assumes set_discussion_names() has already been called !!!
** There are occasions where it it not necessary to build the
** entire msg tree.  This function builds a limited subset of
** the full message tree. 
** All messages associated with the message located at
** file location 'offset' are inserted.  If include_all_topics
** is non-zero, then all topics are inserted as well.
*/
void build_one_topic_tree(SESSION *user, int grp, long target_offset, 
                             MSG_TREE_NODE **msg_ptr, MSG_TREE_NODE **topic_ptr, int *msg_count, 
                             int include_all_topics, int show_teacher_hidden, time_t *current_time)
{
 int fd;
 FILE *fp;
 char inbox_fname[MAX_PATH + 1];  /* full path to central inbox */
 CENTRAL_INBOX_RECORD data, target_data;
 long offset = 0;
 int record_size;
 MSG_TREE_NODE *stored_msg;
 
 record_size = sizeof(CENTRAL_INBOX_RECORD);
 *current_time = time(NULL);
 *msg_ptr = 0;
 *topic_ptr = 0;
 *msg_count = 0;
 
 user_head = 0;     /* these globals should already be set, but just in case */
 tree_head = 0;
  
 user_head = build_user_inbox_list(); 
 
 snprintf(inbox_fname,MAX_PATH + 1,"%s%s",central_discussion_path,INBOX_FNAME);
 fp = fopen(inbox_fname,"r");
 
  if(!fp)                              /* inbox.dat doesn't exist */
  {
    free_user_inbox_list();	  
    return;  /* so there is no list */
  } 


 fd = fileno(fp);
 get_shared_lock(fd, 1);                      /* shared_lock.c */

 /* read the msg we are working with */
 if(fseek(fp, target_offset, SEEK_SET))
    cs_critical_error(ERR_FSEEK_FAILED,"");
 if(fread(&target_data, record_size,1,fp) != 1)
    cs_critical_error(ERR_FREAD_FAILED, "");
 

 /* now read the entire file, inserting only topics
 ** and all msgs related to the target topic
 */
 if(fseek(fp,0,SEEK_SET))
   cs_critical_error(ERR_FSEEK_FAILED,"");
  
 while(fread(&data,record_size,1,fp) == 1)
 {
  if( ((include_all_topics && (data.info.reply_to == -1)) ||
             (data.info.topic_id == target_data.info.topic_id))
                     && msg_should_display(&data,user,grp,*current_time,0,show_teacher_hidden))
  {

   stored_msg =  store_in_msg_tree(&data, offset); 
   if(stored_msg)   /* note program does NOT complain if msg can't be placed! */
    {
     if(data.info.topic_id == target_data.info.topic_id)  /* count msgs associated with THIS topic */
              (*msg_count)++;                       
     if(stored_msg->data.info.msg_id == target_data.info.msg_id)
           *msg_ptr = stored_msg;

     if(stored_msg->data.info.msg_id == target_data.info.topic_id)
                 *topic_ptr = stored_msg;
    }             
  }       
  offset += record_size;
 }
		

 release_lock(fd);   /* shared_lock.c */
 fclose(fp);

 free_user_inbox_list();
 sort_msg_tree(user->team, grp);
 
} 


/* A teacher reading an assignment posted to the ASSIGNMENTS module represents
** a special case.  The tree that needs to be built consists either of a single
** node, which is the assignment itself, or a tree consisting of the assignment and
** all msgs associated with that assignment and sent to a particular student
**
** note that msg_count is the topic plus the (sometimes approximated) number of msgs
** sent by a student in response to the assignment.  This value can be used by the calling
** program to determine whether or not to allow deletion of an assignment
*/
void build_fac_assign_read_tree(SESSION *user,  long target_offset, const char *receiver_username,
                             MSG_TREE_NODE **msg_ptr, MSG_TREE_NODE **topic_ptr, int *msg_count, time_t *current_time)
{
 int fd;
 FILE *fp;
 char inbox_fname[MAX_PATH + 1];  /* full path to central inbox */
 CENTRAL_INBOX_RECORD data, target_data;
 long offset = 0;
 int record_size;
 MSG_TREE_NODE *stored_msg;
 
 record_size = sizeof(CENTRAL_INBOX_RECORD);
 *current_time = time(NULL);
 *msg_ptr = 0;
 *topic_ptr = 0;
 *msg_count = 0;
 
 user_head = 0;     /* these globals should already be set, but just in case */
 tree_head = 0;
  
 user_head = build_user_inbox_list(); 
 
 snprintf(inbox_fname,MAX_PATH + 1,"%s%s",central_discussion_path,INBOX_FNAME);
 fp = fopen(inbox_fname,"r");
  if(!fp)                              /* inbox.dat doesn't exist */
  {
    free_user_inbox_list();	  
    return;  /* so there is no list */
  } 

 
 fd = fileno(fp);
 get_shared_lock(fd, 1);                      /* shared_lock.c */

 /* read the msg we are working with */
 if(fseek(fp, target_offset, SEEK_SET))
    cs_critical_error(ERR_FSEEK_FAILED,"");
 if(fread(&target_data, record_size,1,fp) != 1)
    cs_critical_error(ERR_FREAD_FAILED, "");
 
 /* test for special case... teacher is reading the assignment
 ** from the assign_fac_expand page
 ** only need the assignment 
 */
 if(!strlen(receiver_username))
  {
    if(target_data.info.reply_to != -1)   /* if target is not a topic */
      cs_critical_error(ERR_BLANK_RECIPIENT,"");  /* only topics should have empty receiver_username */
   else
    if(msg_should_display(&data,user,ASSIGNMENTS,*current_time,0,1))
     {
         stored_msg =  store_in_msg_tree(&target_data, target_offset); 
         if(!stored_msg)
            cs_critical_error(ERR_GENERAL_ERROR,"");
         *msg_ptr = stored_msg;
         *topic_ptr = stored_msg;
         *msg_count = 1;  /* this counts the actual topic */
        
          /* The calling program needs to know 
          ** if there have been any messages that have been posted by students to this assignment
          ** we don't need an accurate count, so we stop after finding two
          **/ 
          if(fseek(fp,0,SEEK_SET))
            cs_critical_error(ERR_FSEEK_FAILED,"");
  
          while( (fread(&data,record_size,1,fp) == 1) && *msg_count < 2)
           {
             if( (data.info.topic_id == target_data.info.topic_id) &&
                 (BASIC_STATUS(data.status) != DELETED) &&
                 !strcmp(data.info.sender,data.info.receiver_username) ) /**assignment msgs sent by students have sender the same as receiver_username **/
                              (*msg_count)++;                       
           }                   
      }
   }
  else  /* receiver_username is not empty -add the assignment + all messages to receiver_username */
    {        
     if(fseek(fp,0,SEEK_SET))
        cs_critical_error(ERR_FSEEK_FAILED,"");
  
     while(fread(&data,record_size,1,fp) == 1)
      {
       if( (data.info.topic_id == target_data.info.topic_id) &&
           msg_should_display(&data,user,ASSIGNMENTS,*current_time,0,1))
        {

          /**assignment msgs sent by students have sender the same as receiver_username 
          *** count them.
          **/
          //if( !strcmp(data.info.sender,data.info.receiver_username)  )
           //      (*msg_count)++;             
          
      
          if((data.info.reply_to == -1) ||
            !strcmp(data.info.receiver_username, receiver_username))
           {
             stored_msg =  store_in_msg_tree(&data, offset); 
             (*msg_count)++;             
             if(!stored_msg)
                 cs_critical_error(ERR_GENERAL_ERROR,"");

             if(stored_msg->data.info.msg_id == target_data.info.msg_id)
                    *msg_ptr = stored_msg;

             if(stored_msg->data.info.msg_id == target_data.info.topic_id)
                        *topic_ptr = stored_msg;
           }
        }        
           offset += record_size;
      }   /* while fread() */
		
    }  /* else receiver_username is not empty */
      
 release_lock(fd);   /* shared_lock.c */
 fclose(fp);

 free_user_inbox_list();
 
} 





/* builds a tree (a linked list actually) consisting
** only of -->topics<--- that should be seen by the 
** user.  All messages that are not topics are excluded
*/

void build_topic_only_tree(SESSION *user, int grp)
{
 int fd;
 FILE *fp;
 char inbox_fname[MAX_PATH + 1];  /* full path to central inbox */
 CENTRAL_INBOX_RECORD data;
 long offset = 0;
 time_t current_time;
 int record_size;
 MSG_TREE_NODE *stored_msg;
 
 record_size = sizeof(CENTRAL_INBOX_RECORD);
 current_time = time(NULL);
 
 user_head = 0;     /* these globals should already be set, but just in case */
 tree_head = 0;
 
 user_head = build_user_inbox_list(); 
 
 snprintf(inbox_fname,MAX_PATH + 1,"%s%s",central_discussion_path,INBOX_FNAME);
 fp = fopen(inbox_fname,"r");
 if(!fp)                              /* there is no inbox */
    return;
 
 fd = fileno(fp);
 get_shared_lock(fd, 1);                      /* shared_lock.c */


 /* now read the entire file, inserting only topics
 ** that the current user should see
  */
  
 while(fread(&data,record_size,1,fp) == 1)
 {
  if((data.info.reply_to == -1) 
                     && msg_should_display(&data,user,grp,current_time,0,1))
  {
   stored_msg =  store_in_msg_tree(&data, offset); 
  }       
  offset += record_size;
 }
		

 release_lock(fd);   /* shared_lock.c */
 fclose(fp);

 free_user_inbox_list();
 sort_msg_tree(user->team, grp); 
} 








/* frees the allocated memory
** associated with the message tree
*/

void 
free_msg_tree(MSG_TREE_NODE *root)
{
 if(root)
  {
   if(root->next)	  
    free_msg_tree(root->next);
   if(root->reply) 
    free_msg_tree(root->reply);
   free(root);
   root = (MSG_TREE_NODE *) 0;
  } 
} 


/* this is overkill, now that the tree is being built
 * only with the messages that the user should see.
 * Is the message, msg, unread?
 */
static
int msg_is_unread (MSG_TREE_NODE *msg, const char *username)
{
 static time_t now = 0;

	 
 if(!now)
   now = time(NULL);

 return( !msg->user_data.time_read &&
	 ( (BASIC_STATUS(msg->data.status) == NORMAL) ||
	   ((BASIC_STATUS(msg->data.status) == TIMED_RELEASE) &&
	     (msg->data.release_time < now)) ));
 
}



/* does the message tree, starting with 'root', contain
** at least one unread message?
*/
int has_unread_msgs(MSG_TREE_NODE *root, const char *username)
{
 if(!root)
    return 0;	 
 if(msg_is_unread(root, username))
	 return 1;
 if(has_unread_msgs(root->next,username))
 	 return 1;
 if(has_unread_msgs(root->reply,username))
	 return 1;
 return 0;
} 


/* counts total_msgs and total_unread and total_flagged  messages
** in the message tree pointed to by 'root'
*/
void count_all_msgs(MSG_TREE_NODE *root, int *total_msgs, int *total_unread, int *total_flagged)
{	
 if(!root)
	 return;
 *total_msgs = *total_msgs + 1;
 if(!root->user_data.time_read)
	*total_unread = *total_unread + 1;
 if(root->user_data.flagged)
        *total_flagged = *total_flagged + 1;	
	
 count_all_msgs(root->next,total_msgs, total_unread, total_flagged);
 count_all_msgs(root->reply,total_msgs, total_unread, total_flagged);
}



/* Counts total_msgs and total_unread messages
** that were either sent by or sent to username
** in the message tree pointed to by 'root'
**
** total_teacher_hidden is a count of the msgs that are 
** currently marked 'teacher_hidden' - it's used to support
** hot potatoes exams in the Assignments module.
**
** total_flagged counts the number of messages that have been
** marked with a flag by the user.  NOTE that a flagged message
** is NOT counted if it is a topic.  This is because this function 
** is called from assign_fac_expand where we are deciding whether
** to put a flag icon next to a student's name.  We don't want a
** flag next to every student's name whenever the assignment itself 
** is flagged.
*/
void count_username_msgs(MSG_TREE_NODE *root, const char *username, int *total_msgs, 
							int *total_unread, int *total_teacher_hidden, int *total_flagged)
{	
 if(!root)
	 return;
 if(!strcmp(root->data.info.sender, username)  ||
      !strcmp(root->data.info.receiver_username, username) )
 {     
  *total_msgs = *total_msgs + 1;
  if(!root->user_data.time_read)
	*total_unread = *total_unread + 1;
  if((root->user_data.flagged) && (root->data.info.reply_to != -1)) /* LOOK- don't count flagged topics! */
        *total_flagged = *total_flagged + 1;	
  if(BASIC_STATUS(root->data.status) == TEACHER_HIDDEN)
        *total_teacher_hidden = *total_teacher_hidden + 1;
	
 }
 if(root->next)
    count_username_msgs(root->next,username,total_msgs, total_unread, total_teacher_hidden, total_flagged);
 if(root->reply)   
   count_username_msgs(root->reply,username,total_msgs, total_unread, total_teacher_hidden, total_flagged);
}



/* counts total_msgs, total_unread msgs, and total_flagged_msgs for the 
** topic pointed to by 'topic'
*/	
void count_topic_msgs(MSG_TREE_NODE *topic, int *total_msgs, int *total_unread, int *total_flagged)
{
/* first account for the actual topic */	
 *total_msgs = 1;
 *total_unread = (!topic->user_data.time_read)?1 : 0;
 *total_flagged = topic->user_data.flagged? 1 : 0;

/* then count the msgs that are part of this topic */
 if(topic->reply)
   count_all_msgs(topic->reply,total_msgs, total_unread, total_flagged);
}




static void
record_read_inf_file(CENTRAL_INBOX_RECORD *msg, CONFIG_STRUCT *conf, SESSION *user, time_t now)
{
 char path[MAX_PATH + 1];
 FILE *fp;
 int fd;
 NEWS_PERSON person;
 
 strncpy(person.username,user->username,MAX_USERNAME + 1);
 strncpy(person.realname,user->realname,MAX_NAME + 1);
 person.time_read = now;
 person.user_unlocked = IS_PROTECTED(msg->status);   /* were the attachments protected at the time the msg was read? */


 snprintf(path,MAX_PATH + 1,"%s%s/%s/%s/%s",conf->course_path,PEOPLE_DIR,
                                                          msg->info.sender,discussion_fname,msg->info_fname);

 fp = fopen(path, "a");
 if(!fp)
   cs_critical_error(ERR_FOPEN_WRITE_FAILED, "");

 fd = fileno(fp);  
 get_exclusive_lock(fd, 1);                      /* shared_lock.c */   

 if(fwrite(&person, sizeof(NEWS_PERSON),1,fp) != 1)
      cs_critical_error(ERR_FWRITE_FAILED, "");
 release_lock(fd);                             /* shared_lock.c */
 fclose(fp);
}



/* A record corresponding to msg_id may already exist, since a user
** can 'flag' a message before opening it for the first time.
*/

static void
record_read_local_inbox(int msg_id, time_t now, unsigned char user_unlocked)
{
 FILE *fp;
 USER_INBOX_RECORD data;
 long offset = 0;
 int done = 0;
  
  fp = open_user_inbox("r+");     /* shared_news_util.c */
  
  if(fp)   /* file exists, and open for read/write */
   {
    while(!done)
     {
       offset = ftell(fp);
       if(offset == -1)
         cs_critical_error(ERR_FTELL_FAILED, USER_INBOX_FNAME);
       if(fread(&data, sizeof(USER_INBOX_RECORD), 1, fp) != 1)    /* match not found, write a new record */
         {

           data.msg_id = msg_id;
           data.time_read = now;
           data.user_unlocked = user_unlocked;
           data.user_hidden = 0;
           data.flagged = 0;
           data.future1 = 0;
           data.future2 = 0;
         
           if(fwrite(&data, sizeof(USER_INBOX_RECORD), 1, fp) != 1)
            cs_critical_error(ERR_FWRITE_FAILED, USER_INBOX_FNAME);
           done = 1;
         }
       else   
        if(data.msg_id == msg_id)
         {
           data.time_read = now;
           data.user_unlocked = user_unlocked;
           if(fseek(fp, offset, SEEK_SET))
              cs_critical_error(ERR_FSEEK_FAILED, USER_INBOX_FNAME);
           if(fwrite(&data, sizeof(USER_INBOX_RECORD), 1, fp) != 1)
               cs_critical_error(ERR_FWRITE_FAILED, USER_INBOX_FNAME);
           done = 1;
         }
      }
      fclose(fp);
    }
   else  /* file doesn't exist, write an new record */
    {
     fp = open_user_inbox("a");
     if(fp)
      {
           data.msg_id = msg_id;
           data.time_read = now;
           data.user_unlocked = user_unlocked;
           data.user_hidden = 0;
           data.flagged = 0;
           data.future1 = 0;
           data.future2 = 0;
         
           if(fwrite(&data, sizeof(USER_INBOX_RECORD), 1, fp) != 1)
            cs_critical_error(ERR_FWRITE_FAILED, USER_INBOX_FNAME);
           fclose(fp);
      }        
    }      
     
}          


 


void
mark_msg_as_read(CENTRAL_INBOX_RECORD *msg, CONFIG_STRUCT *conf, SESSION *user)
{
 time_t now;
 
 now = time(NULL);
 record_read_inf_file(msg, conf, user, now);
 record_read_local_inbox(msg->info.msg_id, now, IS_PROTECTED(msg->status));
}



/* marks msg as read using the time specified */
void
mark_msg_as_read_at_time(CENTRAL_INBOX_RECORD *msg, CONFIG_STRUCT *conf, SESSION *user, time_t now)
{
 record_read_inf_file(msg, conf, user, now);
 record_read_local_inbox(msg->info.msg_id, now, IS_PROTECTED(msg->status));
}





/* should this user be able to post a Hot Potatoes exam? */
int allow_hotpot_exam_post(SESSION *user, int grp, int topic)
{
return (user->group == FACULTY) && (grp == ASSIGNMENTS) && (topic == -1);

}



/** does the directory_name start with HOTPOT_EXAM_DIR_PREFIX ?
*** see global.h 
**/
int is_hotpot_exam_dir(const char *directory_name)
{
return (strstr(directory_name,HOTPOT_EXAM_DIR_PREFIX) == directory_name);
}



/** is msg a Hot Potatoes exam?
*** ASSUMES the message is from the ASSIGNMENTS module
**  AND that set_discussion_names() has been called !
**/
int
is_hotpot_exam(CENTRAL_INBOX_RECORD *msg, const char *course_path)
{
 NEWS_INFO info;
 ATTACHMENT attach;
 char info_path[MAX_PATH + 1];
 FILE *fp;
 int fd;
 
 if( (msg->info.reply_to != -1)   /* hot potatoes exam MUST be the topic */
   || (msg->info.attachments != -1))  /* hot potatoes exam MUST be an attached web site */
        return 0;  
 
 /* still here? then we have to open the INF file to examine the unique_fname of the one 
 ** attached file
 */
 snprintf (info_path, MAX_PATH + 1, "%s%s/%s/%s/%s", course_path,PEOPLE_DIR,msg->info.sender,ASSIGNMENTS_DIR,
                                                            msg->info_fname);
 fp = fopen(info_path,"r");
 if(!fp)
    cs_critical_error(ERR_FOPEN_READ_FAILED, "");
 fd = fileno(fp);
 
 get_shared_lock(fd, 1);     /* shared_lock.c */
 if(fread(&info, sizeof(NEWS_INFO), 1, fp) != 1)
      cs_critical_error(ERR_FREAD_FAILED, "");
 if(fread(&attach, sizeof(ATTACHMENT), 1, fp) != 1)
      cs_critical_error(ERR_FREAD_FAILED, "");
 release_lock(fd);   /* shared_lock.c */
 fclose(fp);     

 return(is_hotpot_exam_dir(attach.unique_fname));                   

}



int
modules_enabled (CONFIG_STRUCT *conf)
{
  return (conf->assignments ||
	  conf->syllabus ||
	  conf->discussion ||
	  conf->postoffice ||
	  conf->internet ||
	  conf->lounge ||
	  conf->chat ||
	  conf->anonymous ||
	  conf->team ||
	  conf->team_teach || 
          conf->grades || 
          conf->selftest || 
          conf->lectures ||
          conf->people  ||
          conf->surveys ||
          conf->podcasts);
}


int
only_handouts_enabled (CONFIG_STRUCT *conf)
{
  return (conf->syllabus &&
	  (!conf->assignments &&
	   !conf->discussion &&
	   !conf->postoffice &&
	   !conf->internet &&
	   !conf->lounge &&
	   !conf->chat &&
	   !conf->anonymous &&
	   !conf->team &&
	   !conf->team_teach &&
	   !conf->surveys &&
	   !conf->grades && 
	   !conf->selftest && 
	   !conf->lectures &&
	   !conf->podcasts));
}


/** when a web site is attached to a msg, a symbolic link is created
*** with the name <username>_<course> in the publicly
*** accessible '../images/links/' directory.  this function removes
*** these links if they were not already deleted, but only if 
*** we are not in capture mode
***/


void
remove_stale_symlinks (const char *username, const char *crs, int access)
{
  DIR *dir_p;
  struct dirent *dir_entry_p;
  char matchstring[MAX_FILENAME + 1];
  char link_path[MAX_PATH + 1];
  char modified_course[MAX_PATH + 1];
  
  if(access != ACCESS_CAPTURE)
   {
    course_slash_to_dot(crs, modified_course);    /* shared_util.c */
    snprintf (matchstring, MAX_FILENAME + 1, "%s_%s_", username, modified_course);

    dir_p = opendir ("../images/links");
    if (!dir_p)
       cs_critical_error (ERR_OPENDIR_FAILED, "");

    while (NULL != (dir_entry_p = readdir (dir_p)))
      if (strstr (dir_entry_p->d_name, matchstring) == dir_entry_p->d_name)
        {
	  snprintf (link_path, MAX_PATH + 1, "../images/links/%s",
		  dir_entry_p->d_name);
	  unlink (link_path);
        }
     closedir (dir_p);
  }   
}



void display_protected_status(MSG_TREE_NODE *msg_ptr, const char *username, char *prefix)
{
 int currently_protected;
 int has_attachments;
 int user_read;
 int user_unlocked;
 char timestring[MAX_TIMESTRING + 1];
 char name[MAX_PATH + 1];
 
 currently_protected = IS_PROTECTED(msg_ptr->data.status);  /* IS_PROTECTED macro is in global.h*/
 has_attachments = msg_ptr->data.info.attachments;
 user_read = msg_ptr->user_data.time_read;
 user_unlocked = msg_ptr->user_data.user_unlocked;
 
 if(currently_protected && has_attachments)
  {
    if(!strcmp(username, msg_ptr->data.info.sender))  /* this user sent the msg - show a simple statement that attachments are locked */
      {
	    snprintf(name, MAX_PATH +1, "%slocked_tmsg", prefix);
	    cs_set_int_value(name, 1);
      }    
    else
      if(user_read)
       {
        strftime (timestring, MAX_TIMESTRING, DOW_DATE_TIME_FORMAT, localtime (&(msg_ptr->user_data.time_read)));
        if(user_unlocked)  /* show the user what time they unlocked the msg */
         {
	    snprintf(name, MAX_PATH +1, "%sunlocked_time", prefix);
	    cs_set_value(name, timestring);
          }
        else /* user didn't unlock  - report the problem */
          {
	    snprintf(name, MAX_PATH +1, "%sunlocked_p", prefix);
	    cs_set_value(name, timestring);
          }  
       }    
  } 
} 


/** returns, via 'extension' the portion of 'original_fname'
*** that is to the 'right' of the dot.  (The dot is not included)
***
*** if there is no extension, 'extension' is an empty string.
**/
void
get_extension (const char *original_fname, char *extension)
{
  char *ptr;
  char temp[strlen(original_fname) +1];
  strncpy(temp, original_fname, strlen(original_fname) +1);

  ptr = strrchr (temp, '.');
  if (!ptr)
    *extension = '\0';
  else
    {
      ptr++;
      strncpy (extension, ptr, MAX_FILENAME + 1);
      for (ptr = extension; *ptr; ptr++)
	*ptr = tolower (*ptr);
    }
}



/** assumes set_discussion_names() has been called!!!
*** Reads the message found at offset
**/
void
read_msg_record(long offset, CENTRAL_INBOX_RECORD *data)
{
 FILE *fp;
 int fd;
 char inbox_fname[MAX_PATH + 1];
 
  snprintf(inbox_fname,MAX_PATH + 1,"%s%s",central_discussion_path,INBOX_FNAME);
  fp = fopen(inbox_fname,"r");
  if(!fp)                              /* inbox.dat doesn't exist */
     cs_critical_error(ERR_FOPEN_READ_FAILED, "");

  fd = fileno(fp);
  get_shared_lock(fd, 1);                      /* shared_lock.c */

  if( fseek( fp, offset, SEEK_SET) )
       cs_critical_error(ERR_FSEEK_FAILED, "");
     
  if(fread(data,sizeof(CENTRAL_INBOX_RECORD),1,fp) != 1)
      cs_critical_error(ERR_FREAD_FAILED, "");

 release_lock(fd);   /* shared_lock.c */
 fclose(fp);
}







/*** POSTOFFICE specific routines ***/

void
read_red_file (const char *red_fname, READ_STRUCT * read_info)
{
  FILE *fp;

  fp = fopen (red_fname, "r");
  if (!fp)
    cs_critical_error (ERR_RED_READ_OPEN, "");
  if (fread (read_info, sizeof (READ_STRUCT), 1, fp) != 1)
    cs_critical_error (ERR_RED_READ, "");
  fclose (fp);
}





int
find_longest_line( char *buf, CONFIG_STRUCT *conf)
{
  char *ptr;
  int count =0 ;
  int longest;
  
  longest = editor_width_config(conf->misc);

  for(ptr = buf; *ptr; ptr++)
  {
     if(*ptr == 10)
     {
	if(count > longest)
	  longest = count;
	count = 0;
     }
     else
       count++;
  } 
  return longest;
}


void
do_paste_textarea (const char *clipboard_path,
		   const char *msg_fname,
		   const char *sender_username,  char *sender_realname,
                   SESSION *user, CONFIG_STRUCT *conf)
{
  FILE *fp;
  char fullpath[MAX_PATH + 1];
  struct stat file_stat;
  char *buf;
  int longest;

  snprintf (fullpath, MAX_PATH + 1, "%s%s", clipboard_path, msg_fname);
  if(stat(fullpath, &file_stat))
    cs_critical_error (ERR_STAT_FAILED, fullpath);

  buf = (char*)malloc( (file_stat.st_size +1) * sizeof(char) );
  if(!buf)
    cs_critical_error (ERR_MALLOC_FAILED, "");

  fp = fopen (fullpath, "r");
  if (!fp)
    cs_critical_error (ERR_FOPEN_READ_FAILED, fullpath);

  fread(buf, file_stat.st_size, 1, fp);
  *(buf + file_stat.st_size) = '\0';
  fclose (fp);

  remove_smart_quotes(buf);    /* shared_msg_line.c */
  
  longest = find_longest_line(buf, conf);
  cs_set_int_value("msg.textarea.cols", longest);

  if (strcmp (user->username, sender_username))
     cs_set_value("msg.textarea.feedback", sender_realname);


  cs_set_value("msg.textarea.body", buf);
  free(buf);

}

char*
replace_quot( char *orig_subject)
{
  char *subject =0, *ptr;
  int j =0;

  subject = (char*)malloc(sizeof(char) * strlen(orig_subject) * 6 +1);
  for (ptr = orig_subject; *ptr; ptr++)
  {
    if (*ptr == '"')
    {
        subject[j] = '&';
        subject[j+1] = 'q';
        subject[j+2] = 'u';
        subject[j+3] = 'o';
        subject[j+4] = 't';
        subject[j+5] = ';';
        j = j+5;
    }
    else
	subject[j]= *ptr;
    subject[j+1]='\0';
    j++;
  }
  return subject;
}


char*
add_mark_to_msg( char *buf, int num_line)
{
   char *msg =0;
   char *ptr;
   int i =0;
   int string_len;
   
   string_len = strlen(buf) + num_line + 2;   /* extra byte helps with empty messages */
   
   msg = (char*)malloc(sizeof(char) * string_len);
   if(!msg)
       cs_critical_error(ERR_MALLOC_FAILED, "add_mark_to_msg()");
       
   msg[i] = '>';
   i++;
   for(ptr = buf; *ptr; ptr++)
   {
      if(*ptr == 10)
       {
	  msg[i] = *ptr;
	  msg[i+1] = '>';
	  i = i +1;
       }
      else
	  msg[i] = *ptr;
      msg[i+1] = '\0';
      i++;
   }
   
   msg[string_len - 1] = '\0';   /* make sure its null terminated */
   return msg;
}


int
find_num_lines( char *buf)
{
  char *ptr;
  int line =0;

  for(ptr = buf; *ptr; ptr++)
  {
     if(*ptr == 10)
	  line++;
  }
  return line;
}




static RECEIVER_NODE *
alloc_new_node(const char *list_name)
{
   char temp[strlen(list_name) +1];
   char *ptr;
   RECEIVER_NODE *rptr =0;

   strncpy(temp, list_name, strlen(list_name) +1);
   ptr = strchr(temp, ':');
   if(!ptr)
      cs_critical_error(ERR_PARAM_FORMAT, "");
   *ptr = '\0';
    ptr++;

   rptr = (RECEIVER_NODE *)malloc(sizeof(RECEIVER_NODE));
   if(!rptr)
      cs_critical_error(ERR_MALLOC_FAILED, "");
   strncpy(rptr->username, temp, MAX_USERNAME +1);   
   strncpy(rptr->realname, ptr, MAX_NAME +1);   
   rptr->next =0;
   return rptr;
}





void
create_rec_list(const char *name, RECEIVER_NODE **head)
{
#define MAX_TMP_NAME 30
   char *str =0;
   char myname[MAX_TMP_NAME];
   RECEIVER_NODE *rptr, *current =0;
   HDF *obj;

   snprintf(myname, MAX_TMP_NAME, "Query.%s.0", name); 
   str = hdf_get_value(global_cgi->hdf, myname, 0);
   if(str)
   {
      snprintf(myname, MAX_TMP_NAME, "Query.%s", name); 
      obj = hdf_get_child(global_cgi->hdf, myname);
      while(obj)
      {
	  str =obj->value;
	  if(str && strlen(str))
	  {
	    rptr =  alloc_new_node(str);
	    if(!*head)
		 *head = rptr;
	    else
		  current->next = rptr;
	    current = rptr;
	  }
	  obj = obj->next;
      } 
   }
   else
   {
      snprintf(myname, MAX_TMP_NAME, "Query.%s", name); 
      str = hdf_get_value(global_cgi->hdf, myname, 0);
      if(str)
      {
	    rptr =  alloc_new_node(str);
	    *head = rptr; 
      }
   }
#undef MAX_TMP_NAME 

}




static
void cs_set_prefixed_value(const char *prefix, char *name, char *value)
{
#define  MAX_HDF_NAME 100

 char prefixed_name[MAX_HDF_NAME + 1];
 int needs_dot;

 /* If he prefix passed in doesn't already end in a dot, we'll have to add one */ 
 needs_dot = *prefix && (prefix[strlen(prefix) - 1]  != '.');

 snprintf(prefixed_name, MAX_HDF_NAME + 1, "%s%s%s",
                                          prefix,
                                          needs_dot? ".":"",
                                          name);
 cs_set_value(prefixed_name, value);


#undef MAX_HDF_NAME 
}





void
set_news_msg_header_data(MSG_TREE_NODE *msg, char *name_prefix, 
                                   int is_teacher_hidden, int is_read_only, char *topic_subject)
{
 char timestring[MAX_TIMESTRING + 1];
 char *escaped_string = 0;

 snprintf(timestring, MAX_TIMESTRING + 1, "%ld", msg->offset);
 cs_set_prefixed_value(name_prefix, "offset", timestring);
 
 if(msg->data.release_time > 0)
  {
    strftime (timestring, MAX_TIMESTRING, DOW_DATE_TIME_FORMAT,
	      localtime (&(msg->data.release_time))); 
    cs_set_prefixed_value(name_prefix, "release_time", timestring);
   } 
    
      
 if(msg->data.unrelease_time > 0)
  {
    strftime (timestring, MAX_TIMESTRING, DOW_DATE_TIME_FORMAT,
	      localtime (&(msg->data.unrelease_time))); 
    cs_set_prefixed_value(name_prefix, "unrelease_time", timestring);
    
   } 

 cs_set_prefixed_value(name_prefix, "info_fname", msg->data.info_fname);

 /* teacher_hidden and read_only values must be passed to this function
 ** since they are stored in the topic, not necessarily in this msgs record
 */
  if(is_teacher_hidden)
      cs_set_prefixed_value(name_prefix, "teacher_hidden", "1"); 
      
  if(is_read_only)    
    cs_set_prefixed_value(name_prefix, "read_only", "1");
    
    
  if(*topic_subject)
   {
     cs_set_prefixed_value(name_prefix,"topic_subject", topic_subject);

     escaped_string = 0;
     cs_html_escape_string(topic_subject, &escaped_string);
     if(escaped_string)
      {
        cs_set_prefixed_value(name_prefix, "escaped_topic_subject", (char *)escaped_string);
        free(escaped_string);
        escaped_string = 0;
      }
   }  
     
    
  if(IS_PROTECTED(msg->data.status))    /* IS_PROTECTED macro is in global.h */
      cs_set_prefixed_value(name_prefix,"protected", "1");
      
      
  cs_set_prefixed_value(name_prefix, "sender", msg->data.info.sender);      
  cs_set_prefixed_value(name_prefix, "sender_realname", msg->data.info.sender_realname);        
  if(*msg->data.info.receiver_username)
     cs_set_prefixed_value(name_prefix, "receiver_username", msg->data.info.receiver_username); 
     
  cs_set_prefixed_value(name_prefix, "subject", msg->data.info.subject);
  
  escaped_string = 0;
  cs_html_escape_string(msg->data.info.subject, &escaped_string);
  if(escaped_string)
   {
    cs_set_prefixed_value(name_prefix, "escaped_subject", (char *)escaped_string);
    free(escaped_string);
   }
   

   strftime (timestring, MAX_TIMESTRING, DOW_DATE_TIME_FORMAT,
	      localtime (&(msg->data.info.time_sent))); 
   cs_set_prefixed_value(name_prefix, "time_sent", timestring);
    
   
   snprintf(timestring, MAX_TIMESTRING + 1, "%d", msg->data.info.attachments);
   cs_set_prefixed_value(name_prefix, "attachments", timestring);
   
   
   snprintf(timestring, MAX_TIMESTRING + 1, "%c", msg->data.info.sender_team);
   cs_set_prefixed_value(name_prefix, "sender_team", timestring);

   snprintf(timestring, MAX_TIMESTRING + 1, "%d", msg->data.info.topic_id);
   cs_set_prefixed_value(name_prefix, "topic_id", timestring);
   
   snprintf(timestring, MAX_TIMESTRING + 1, "%d", msg->data.info.msg_id);
   cs_set_prefixed_value(name_prefix, "msg_id", timestring);


   snprintf(timestring, MAX_TIMESTRING + 1, "%d", msg->data.info.reply_to);
   cs_set_prefixed_value(name_prefix, "reply_to", timestring);
   
   if(msg->data.info.reply_to == -1)
      cs_set_prefixed_value(name_prefix, "is_topic", "1");
      
        
   if(msg->user_data.time_read)
    {
     strftime (timestring, MAX_TIMESTRING, DOW_DATE_TIME_FORMAT,
	      localtime (&(msg->user_data.time_read))); 
     cs_set_prefixed_value(name_prefix, "time_read", timestring);
    }
    
     
   if(msg->user_data.user_unlocked)
      cs_set_prefixed_value(name_prefix, "user_unlocked", "1");
             
   if(msg->user_data.user_hidden)
      cs_set_prefixed_value(name_prefix, "user_hidden", "1");
      
           
   
}









static 
void set_locked_attachments_url(const char *course, const char *key, 
                            int grp,MSG_TREE_NODE *msg_ptr, const char *name_prefix)
{
char url[MAX_PATH + 1];

 cs_set_prefixed_value(name_prefix, "attachments_locked", "1");
 snprintf(url,MAX_PATH + 1, "%s?crs=%s&amp;id=%s&amp;grp=%d&amp;loc=%ld&amp;unlock=0",
                              "news_unlock",
                              course,
                              key,
                              grp,
                              msg_ptr->offset); 
 
 cs_set_prefixed_value(name_prefix, "unlock_url", url);   /* shared_cs_util.h */
 
}





static
void  set_normal_website_url(const char *course, const char *key, 
                                    const char *sender, const char *info_fname, 
                                    int grp,
                                    long offset, int  is_fac_teamteach,
                                    char *assign_read_student,
                                    char team, const char *from,
                                    const char *name_prefix)
{
 char url[MAX_PATH + 1];
 char teamstring[2];
 
 *teamstring = team;
 *(teamstring + 1) = '\0';

  snprintf(url, MAX_PATH + 1, "%s?crs=%s&amp;id=%s&amp;sender=%s&amp;inf=%s&amp;grp=%d&amp;read=1&amp;loc=%ld%s%s&amp;from=%s%s%s",
                  "web_page_start",
                  course,
                  key,
                  sender,
                  info_fname,
                  grp,
                  offset,
                  is_fac_teamteach? "&amp;team=" : "",
                  is_fac_teamteach? teamstring : "",
                  from,
                  *assign_read_student? "&amp;student=":"",
                  assign_read_student);
                  
  cs_set_prefixed_value(name_prefix, "normal_website_url", url);

                                    
}





static
void  set_survey_url(const char *course, const char *key, 
                                    const char *sender, const char *info_fname, 
                                    long offset, const char *name_prefix)
{
char url[MAX_PATH + 1];
      
snprintf(url, MAX_PATH + 1, "%s?crs=%s&amp;id=%s&amp;sender=%s&amp;inf=%s&amp;loc=%ld",
                  "survey_take_verify",
                  course,
                  key,
                  sender,
                  info_fname,
                  offset);

cs_set_prefixed_value(name_prefix, "survey_url", url);            /* shared_cs_util.h */
                                    
}




static
void  set_hotpot_exam_url(const char *course, const char *key, 
                                    const char *sender, const char *info_fname, 
                                    long offset, const char *name_prefix)
{
char url[MAX_PATH + 1];
      
snprintf(url, MAX_PATH + 1, "%s?crs=%s&amp;id=%s&amp;sender=%s&amp;inf=%s&amp;loc=%ld",
                  "hotpot_exam_verify",
                  course,
                  key,
                  sender,
                  info_fname,
                  offset);

cs_set_prefixed_value(name_prefix,"hotpot_exam_url", url);     /* shared_cs_util.h */
                                    
}





void
set_news_attachment_data (MSG_TREE_NODE *msg_ptr,  char *topic_subject,
		      const char *course, const char *key, int grp,
		      int website_attached, char *from,
		      char team, int is_fac_teamteach, char *assign_read_student,
		      CONFIG_STRUCT *conf, int attachments_locked, 
		      const char *username, ATTACHMENT_NODE *attachment_head,
		      const char *name_prefix)
{

#define MAX_HDF_NAME 100

  ATTACHMENT_NODE *aptr;

  char offset_string[20];
  char extension[MAX_FILENAME + 1];
  int i;
  char url[MAX_PATH + 1];
  char hdf_name[MAX_HDF_NAME + 1];
      
  if (grp == ANONYMOUS_DISCUSSION)	/* for anonymous groups we need to send the offset instead of sender's name */
    snprintf (offset_string, 20, "%ld", msg_ptr->offset);


  if(website_attached)
     cs_set_prefixed_value(name_prefix, "website_attached", "1");   


  if(attachments_locked)  /* if IS_PROTECTED and has attachments and is unread */
    set_locked_attachments_url(course,key,grp,msg_ptr, name_prefix);

  if (website_attached)
       {
         if(grp == SURVEYS)
           {
             set_survey_url(course, key, msg_ptr->data.info.sender, msg_ptr->data.info_fname, msg_ptr->offset, name_prefix);
           }
    
          /* if this is a 'normal website attachment 
          ** normal attachments have unique_fnames that start with "dir"
          ** also if this is not the ASSIGNMENTS module or the SURVEYS module, treat all attached websites as 
          ** normal website attachments
           */
     
         else  /* is this a normal website? */ 
           if( (grp != ASSIGNMENTS) || (attachment_head && 
                (strstr(attachment_head->data.unique_fname,"dir") == attachment_head->data.unique_fname)))
             {
               set_normal_website_url(course, key, 
                                        msg_ptr->data.info.sender, 
                                        msg_ptr->data.info_fname, 
                                        grp, msg_ptr->offset,
                                        is_fac_teamteach,
                                        assign_read_student,
                                        team,
                                        from,
                                        name_prefix);
  
              }
            else  /* assume this is a HOT POTATOES exam in the assignments module*/
              {
                cs_set_prefixed_value(name_prefix, "hotpot_exam", "1");   /* shared_cs_util.h */
                set_hotpot_exam_url(course, key, msg_ptr->data.info.sender, msg_ptr->data.info_fname, msg_ptr->offset, name_prefix);
               }

       } 
  else     /* website not attached */
      {	       /***** attachments ***/
        if (attachment_head)
	  {
            /* note the ext=<file_extension> parameter isn't used by the send_file program
            ** it's there to help offline browser programs to determine the type of file
            ** being sent
             */

	    for (aptr = attachment_head, i = 0; aptr; aptr = aptr->next, i++)
             {
                snprintf(hdf_name, MAX_HDF_NAME + 1, "file.%d.orig_fname", i);
                cs_set_prefixed_value(name_prefix, hdf_name, aptr->data.orig_fname);  /* shared_cs_util.h */
              
                get_extension(aptr->data.orig_fname, extension);   /* shared_news_util.c */

                if(!attachments_locked)
                 {
	          snprintf
	            (url, MAX_PATH + 1, "%s?crs=%s&amp;id=%s&amp;user=%s&amp;fname=%s&amp;info=%s&amp;attach=%d&amp;grp=%d&amp;ext=.%s",
	             "send_file", course, key,
	             grp == ANONYMOUS_DISCUSSION ? offset_string : msg_ptr->data.info.sender, 
	             aptr->data.unique_fname,
	             msg_ptr->data.info_fname,
	             msg_ptr->data.info.attachments, grp, extension);
	       
                  snprintf(hdf_name, MAX_HDF_NAME + 1, "file.%d.url", i);  
	          cs_set_prefixed_value(name_prefix, hdf_name, url);                   /* shared_cs_util.h */
	          
	          /* if this is an mp3 file, 
	          ** provide url for mp3 player
	          */
	          if(!strcmp(extension,"mp3"))   /* custom.h */
		  {

                    cs_set_server_name();
                    snprintf(url, MAX_PATH +1, "podcast_launch_framaplayer?crs=%s&id=%s&title=%s&sender=%s&fname=%s&inf=%s&grp=%d&realname=%s", 
                                    course, key, msg_ptr->data.info.subject, msg_ptr->data.info.sender, aptr->data.unique_fname,
                                    msg_ptr->data.info_fname, grp, msg_ptr->data.info.sender_realname
                                    );

                    snprintf(hdf_name, MAX_HDF_NAME + 1, "file.%d.wimpy_button_url", i);  
	            cs_set_prefixed_value(name_prefix, hdf_name, url);                   /* shared_cs_util.h */
	          } 
	         } 
	       
             }
	  }
      }
    
   
      
#undef MAX_HDF_NAME      
}

static
void get_time_assignment_opened(const char *student, const char *assignment_sender, const char *info_fname, 
					CONFIG_STRUCT *conf, time_t *opened_time, unsigned char *user_unlocked)
{
 FILE *fp;
 int fd;
 char infopath[MAX_PATH + 1];
 NEWS_INFO infofile_info;
 ATTACHMENT junker;   /* used to swallow attachment data from the info file */
 int i, found;
 NEWS_PERSON person;
 
  *opened_time = 0;
  *user_unlocked = 0;
  
  snprintf (infopath, MAX_PATH + 1, "%s%s/%s/%s/%s", conf->course_path,PEOPLE_DIR,
	    assignment_sender, discussion_fname, info_fname);

  fp = fopen (infopath, "r");
  if (!fp)
     return;        
  fd = fileno(fp);
  get_shared_lock(fd,1);         /* shared_lock.c */
    
  if (fread (&infofile_info, sizeof (NEWS_INFO), 1, fp) != 1)
    cs_critical_error (ERR_FREAD_FAILED, infopath);

  /* if a website was attached, we need to 'swallow' the attachment */
  if (infofile_info.attachments == -1)  /* -1 signifies an attached web page */
        infofile_info.attachments = 1;  

  for (i = 1; i <= infofile_info.attachments; i++)
      if (fread (&junker, sizeof (ATTACHMENT), 1, fp) != 1)
	cs_critical_error (ERR_FREAD_FAILED, infopath);

 /** now the list of people who have read this message, looking for this student */
  found = 0;
  while (fread (&person, sizeof (NEWS_PERSON), 1, fp) == 1)
    {
      if(!strcmp(person.username, student) )
       {
        found = 1;
        break;
       }
    }

   release_lock(fd);   /* shared_lock.c */ 
   fclose(fp);

  if(found)
    {
      *opened_time = person.time_read;
      *user_unlocked = person.user_unlocked? 1: 0;
    }
}             





static
int get_assignment_elapsed_times( MSG_TREE_NODE *msg_ptr, 
                       MSG_TREE_NODE *topic_ptr, 
                       CONFIG_STRUCT *conf,
                       time_t *opened_time,
                       int *days,
                       int *hours,
                       int *minutes,
                       unsigned char *user_unlocked)
                       
{

#define SECONDS_PER_DAY 86400
#define SECONDS_PER_HOUR 3600
#define SECONDS_PER_MIN  60

 time_t elapsed_time;
 int msg_was_opened = 0;

 get_time_assignment_opened(msg_ptr->data.info.sender, 
                                      topic_ptr->data.info.sender, 
                                               topic_ptr->data.info_fname, conf, opened_time, user_unlocked);

  msg_was_opened = opened_time? 1 : 0;
  if(msg_was_opened)
   {
    elapsed_time = msg_ptr->data.info.time_sent - *opened_time;
    *days = elapsed_time / SECONDS_PER_DAY;
    
    elapsed_time = elapsed_time % SECONDS_PER_DAY;
    *hours = elapsed_time / SECONDS_PER_HOUR;
        
    elapsed_time = elapsed_time % SECONDS_PER_HOUR;
    *minutes = elapsed_time / SECONDS_PER_MIN;

   }   
       
  return msg_was_opened;       
#undef SECONDS_PER_DAY
#undef SECONDS_PER_HOUR
#undef SECONDS_PER_MIN

}





void
set_assign_read_heading_data (MSG_TREE_NODE *msg_ptr,  char *name_prefix, MSG_TREE_NODE *topic_ptr,
		       char *course,  char *key, CONFIG_STRUCT *conf, int teacher_hidden, 
	                          int can_write, int read_only)
{
  #define MAX_TMP_NAME 30

  char timestring[MAX_TIMESTRING + 1];
  time_t opened_time;                   /* what time did this user open the Assignment that this message belongs to */
  unsigned char user_unlocked;          /* did the user have to 'unlock' the attachments when the Assignment was opened */ 
  int days, hours, minutes;



  set_news_msg_header_data(msg_ptr, name_prefix, teacher_hidden, read_only, topic_ptr->data.info.subject);   /* shared_news_util.c */

  /* if the message was posted by a student, set data that indicates the 
  ** time elapsed between when this message was sent and when the ASSIGNMENT
  ** was first opened
  **/
  if(!strcmp(msg_ptr->data.info.sender, msg_ptr->data.info.receiver_username))
      {
         
        if(get_assignment_elapsed_times(msg_ptr, topic_ptr, conf, &opened_time, &days, &hours, &minutes, &user_unlocked))
         {
           strftime (timestring, MAX_TIMESTRING, DOW_DATE_TIME_FORMAT, localtime (&opened_time));
           cs_set_prefixed_value(name_prefix, "assign_open_time", timestring);

           snprintf(timestring, MAX_TIMESTRING, "%d", days);
           cs_set_prefixed_value(name_prefix, "open_elapsed_days", timestring);


           snprintf(timestring, MAX_TIMESTRING, "%d", hours);
           cs_set_prefixed_value(name_prefix, "open_elapsed_hours", timestring);


           snprintf(timestring, MAX_TIMESTRING, "%d", minutes);
           cs_set_prefixed_value(name_prefix, "open_elapsed_minutes", timestring);

           cs_set_prefixed_value(name_prefix, "user_unlocked", user_unlocked? "1":"0");
         }         
         
      }
      
  

  /* if this is NOT a topic, and it is TEACHER_HIDDEN, then
  ** it's an automatically generated Hot Potatoes test result
  ** that has not been released to the student
  ** provide a form to release the message , if the teacher has
  ** write access
  **/
  if( (BASIC_STATUS (msg_ptr ->data.status) == TEACHER_HIDDEN)
      && (msg_ptr->data.info.reply_to != -1) && can_write)
  {
    cs_set_prefixed_value(name_prefix, "include_hotpot_release_form", "1");    
    
  }
  else

   /* need to notify the teacher if the message is hidden from students */
  if(teacher_hidden || read_only)
  {
	if(teacher_hidden)
           cs_set_prefixed_value(name_prefix, "teacher_hidden", "1");
	else
           cs_set_prefixed_value(name_prefix, "read_only", "1");
  } 
                            
  #undef MAX_TMP_NAME 
}



/** Lectern is an obscure, but very interesting lecture recorder.
*** It is/was sold from:  http://www.rether.com
***
*** A Lectern lecture can be uploaded to any Manhattan module, and
*** Manhattan will present the voice file, the Real Media source, and
*** the slides html file.
***
*** This file returns 1 if the website just uploaded appears to be a 
*** lectern lecture.  Assumes set_discussion_names() was called.
**/

int  is_lectern_lecture()
{
  char path[MAX_PATH + 1];

  snprintf (path, MAX_PATH + 1, "%s/%s/voice.mp3", discussion_path,ZIP_TMP_DIRNAME);
  if(!file_exists(path))   /* shared_file_util.c */
     return 0;

  snprintf (path, MAX_PATH + 1, "%s/%s/present.rp", discussion_path,ZIP_TMP_DIRNAME);
  if(!file_exists(path))   /* shared_file_util.c */
     return 0;


  snprintf (path, MAX_PATH + 1, "%s/%s/index.smil", discussion_path,ZIP_TMP_DIRNAME);
  if(!file_exists(path))   /* shared_file_util.c */
     return 0;


  snprintf (path, MAX_PATH + 1, "%s/%s/printslides.html", discussion_path,ZIP_TMP_DIRNAME);
  if(!file_exists(path))   /* shared_file_util.c */
     return 0;
     
  return 1;

}




void
get_orig_fname (const char *full_orig_name, char *orig_fname)
{

  char *temp_fname;
  char *ptr;
  int chars_to_copy;
  char *extension;
   
  temp_fname = (char *) malloc(strlen(full_orig_name) + 1);
  if(!temp_fname)
      cs_critical_error(ERR_MALLOC_FAILED, "get_orig_fname()");
      

 /* Depending on the browser and the OS, full_orig_name might contain
 ** paths that are separated by backslashes, by forward slashes, or 
 ** no path info at all.  We need to get just the filename...
 **/

  ptr = strrchr (full_orig_name, '\\');   /* look for a backslash from the end of the string */
  if (ptr)                                /* found, then ... */
    strcpy (temp_fname, ptr + 1);         /* filename is to right of the backslash */
  else                                    /* no backslash found */
    {
      ptr = strrchr (full_orig_name, '/');    /* look for a forward slash */
      if (ptr)			              /* found, then ... */
	strcpy (temp_fname, ptr + 1);         /* filename is to right of forward slash */
      else                                    /* no backslash or forward slashes found */
	strcpy (temp_fname, full_orig_name);  /* entire string is the filename */
    }
 
 
  /* If the filename is longer than allowed, we must shorten it
  ** If possible, we'll preserve the file's extension 
  **/
 
  if(strlen(temp_fname) > MAX_FILENAME)   
   {
     extension = strrchr(temp_fname, '.');   /* find start of file extension */
     
     /* If no file extension found, or the file extension is as large as we allow
     ** for an entire filename, simply truncate the filename 
     **/
     if(!extension  || (extension && (strlen(extension) >= MAX_FILENAME) )) 
       {
        *(temp_fname + MAX_FILENAME) = '\0';  /*simply shorten the filename */
        strncpy(orig_fname, temp_fname, MAX_FILENAME + 1);
       } 
     else   /* preserve the file extension, but truncate the rest of the file */
       {
        chars_to_copy = MAX_FILENAME - strlen(extension);
        *(temp_fname + chars_to_copy) = '\0';
        snprintf(orig_fname,MAX_FILENAME + 1, "%s%s", temp_fname, extension);
       }
   }
  else    /** filename was not too long */
      strncpy(orig_fname, temp_fname, MAX_FILENAME + 1);
      
      
  /* make double-sure the file is truncate ! */    
  *(orig_fname + MAX_FILENAME) = '\0';
            
  free(temp_fname);
              
}       


int get_unique_fd(const char *base_dir, const char *filetype, char *unique_fname, time_t *time_sent)
{
  char template[MAX_PATH + 1];
  char timestring[MAX_TIMESTRING + 1];
  int fd;
  char *ptr;
  
  strftime (timestring, MAX_TIMESTRING, "%m%d%Y%H%M%S",localtime (time_sent));

  snprintf (template, MAX_PATH + 1, "%s%s_%s_XXXXXX", base_dir,filetype, timestring);
  
  fd = mkstemp(template);
  if(fd == -1)
      cs_critical_error(ERR_MKSTEMP_FAILED, template);
  change_mkstemp_permission(template);   /* shared_file_util.c */
  ptr = strrchr(template,'/');
  if(!ptr)
    cs_critical_error(ERR_GENERAL_ERROR, "get_unique_fd()");
  ptr++;
  strncpy(unique_fname, ptr,MAX_FILENAME + 1);
  if(fchmod(fd,S_IWGRP|S_IRGRP|S_IRUSR|S_IWUSR))
    cs_critical_error(ERR_CHMOD_FAILED,"");
  return fd;
}  


void
write_cgi_file (FILE *infp, long size, const char *unique_fname)
{

#define CHUNKSIZE 2048
  FILE  *outfp;
  long bytes_read;
  long bytes_left;
  char buffer[CHUNKSIZE];
  char filepath[MAX_PATH + 1];

  snprintf (filepath, MAX_PATH + 1, "%s/%s", discussion_path, unique_fname);

  outfp = fopen (filepath, "w");
  if (!outfp)
      cs_critical_error (ERR_FOPEN_WRITE_FAILED, "write_cgi_file");


  bytes_left = size;
  bytes_read = CHUNKSIZE;
  while (bytes_left)
    {
      if (bytes_left < CHUNKSIZE)
	bytes_read = bytes_left;
      if (fread (buffer, bytes_read, 1, infp) != 1)
	 cs_critical_error (ERR_FREAD_FAILED, "");
      bytes_left -= bytes_read;
      if (fwrite (buffer, bytes_read, 1, outfp) != 1)
	  cs_critical_error (ERR_FWRITE_FAILED, "");

    }
  fclose (outfp);
#undef CHUNKSIZE

}



/** Did the user abort an upload?
*** If a user stops their web browser, or navigates away from a page before
*** a file upload is complete, there's the possiblility only part of an
*** attahed file will be uploaded.
***
*** This check *should* test whether or not the full file was uploaded.
*** The cs_critical_error() screen will probably never be seen by a user
*** since they have ditched out anyway.
***/

void
check_incomplete_upload()
{
 if(global_cgi->data_read < global_cgi->data_expected)
    cs_critical_error(ERR_GENERAL_ERROR, "Upload Cancelled by User?");
}
