/*
 * RSS notifier for CUPS.
 *
 * Copyright 2007-2015 by Apple Inc.
 * Copyright 2007 by Easy Software Products.
 *
 * Licensed under Apache License v2.0.  See the file "LICENSE" for more information.
 */

/*
 * Include necessary headers...
 */

#include <cups/cups.h>
#include <sys/stat.h>
#include <cups/language.h>
#include <cups/string-private.h>
#include <cups/array.h>
#include <sys/select.h>
#include <cups/ipp-private.h>	/* TODO: Update so we don't need this */


/*
 * Structures...
 */

typedef struct _cups_rss_s		/**** RSS message data ****/
{
  int		sequence_number;	/* notify-sequence-number */
  char		*subject,		/* Message subject/summary */
		*text,			/* Message text */
		*link_url;		/* Link to printer */
  time_t	event_time;		/* When the event occurred */
} _cups_rss_t;


/*
 * Local globals...
 */

static char		*rss_password;	/* Password for remote RSS */


/*
 * Local functions...
 */

static int		compare_rss(_cups_rss_t *a, _cups_rss_t *b);
static void		delete_message(_cups_rss_t *msg);
static void		load_rss(cups_array_t *rss, const char *filename);
static _cups_rss_t	*new_message(int sequence_number, char *subject,
			             char *text, char *link_url,
				     time_t event_time);
static const char	*password_cb(const char *prompt);
static int		save_rss(cups_array_t *rss, const char *filename,
			         const char *baseurl);
static char		*xml_escape(const char *s);


/*
 * 'main()' - Main entry for the test notifier.
 */

int					/* O - Exit status */
main(int  argc,				/* I - Number of command-line arguments */
     char *argv[])			/* I - Command-line arguments */
{
  int		i;			/* Looping var */
  ipp_t		*event;			/* Event from scheduler */
  ipp_state_t	state;			/* IPP event state */
  char		scheme[32],		/* URI scheme ("rss") */
		username[256],		/* Username for remote RSS */
		host[1024],		/* Hostname for remote RSS */
		resource[1024],		/* RSS file */
		*options;		/* Options */
  int		port,			/* Port number for remote RSS */
		max_events;		/* Maximum number of events */
  http_t	*http;			/* Connection to remote server */
  http_status_t	status;			/* HTTP GET/PUT status code */
  char		filename[1024],		/* Local filename */
		newname[1024];		/* filename.N */
  struct stat	fileinfo;		/* Local file information */
  cups_lang_t	*language;		/* Language information */
  ipp_attribute_t *printer_up_time,	/* Timestamp on event */
		*notify_sequence_number,/* Sequence number */
		*notify_printer_uri;	/* Printer URI */
  char		*subject,		/* Subject for notification message */
		*text,			/* Text for notification message */
		link_url[1024],		/* Link to printer */
		link_scheme[32],	/* Scheme for link */
		link_username[256],	/* Username for link */
		link_host[1024],	/* Host for link */
		link_resource[1024];	/* Resource for link */
  int		link_port;		/* Link port */
  cups_array_t	*rss;			/* RSS message array */
  _cups_rss_t	*msg;			/* RSS message */
  char		baseurl[1024];		/* Base URL */
  fd_set	input;			/* Input set for select() */
  struct timeval timeout;		/* Timeout for select() */
  int		changed;		/* Has the RSS data changed? */
  int		exit_status;		/* Exit status */


  fprintf(stderr, "DEBUG: argc=%d\n", argc);
  for (i = 0; i < argc; i ++)
    fprintf(stderr, "DEBUG: argv[%d]=\"%s\"\n", i, argv[i]);

 /*
  * See whether we are publishing this RSS feed locally or remotely...
  */

  if (httpSeparateURI(HTTP_URI_CODING_ALL, argv[1], scheme, sizeof(scheme),
                      username, sizeof(username), host, sizeof(host), &port,
		      resource, sizeof(resource)) < HTTP_URI_OK || strstr(resource, "../") != NULL)
  {
    fprintf(stderr, "ERROR: Bad RSS URI \"%s\".\n", argv[1]);
    return (1);
  }

  max_events = 20;

  if ((options = strchr(resource, '?')) != NULL)
  {
    *options++ = '\0';

    if (!strncmp(options, "max_events=", 11))
    {
      max_events = atoi(options + 11);

      if (max_events <= 0)
        max_events = 20;
    }
  }

  rss = cupsArrayNew((cups_array_func_t)compare_rss, NULL);

  if (host[0])
  {
   /*
    * Remote feed, see if we can get the current file...
    */

    int	fd;				/* Temporary file */


    if ((rss_password = strchr(username, ':')) != NULL)
      *rss_password++ = '\0';

    cupsSetPasswordCB(password_cb);
    cupsSetUser(username);

    if ((fd = cupsTempFd(filename, sizeof(filename))) < 0)
    {
      fprintf(stderr, "ERROR: Unable to create temporary file: %s\n",
              strerror(errno));

      return (1);
    }

    if ((http = httpConnect(host, port)) == NULL)
    {
      fprintf(stderr, "ERROR: Unable to connect to %s on port %d: %s\n",
              host, port, strerror(errno));

      close(fd);
      unlink(filename);

      return (1);
    }

    status = cupsGetFd(http, resource, fd);

    close(fd);

    if (status != HTTP_OK && status != HTTP_NOT_FOUND)
    {
      fprintf(stderr, "ERROR: Unable to GET %s from %s on port %d: %d %s\n",
	      resource, host, port, status, httpStatus(status));

      httpClose(http);
      unlink(filename);

      return (1);
    }

    strlcpy(newname, filename, sizeof(newname));

    httpAssembleURI(HTTP_URI_CODING_ALL, baseurl, sizeof(baseurl), "http",
                    NULL, host, port, resource);
  }
  else
  {
    const char	*cachedir,		/* CUPS_CACHEDIR */
		*server_name,		/* SERVER_NAME */
		*server_port;		/* SERVER_PORT */


    http = NULL;

    if ((cachedir = getenv("CUPS_CACHEDIR")) == NULL)
      cachedir = CUPS_CACHEDIR;

    if ((server_name = getenv("SERVER_NAME")) == NULL)
      server_name = "localhost";

    if ((server_port = getenv("SERVER_PORT")) == NULL)
      server_port = "631";

    snprintf(filename, sizeof(filename), "%s/rss%s", cachedir, resource);
    snprintf(newname, sizeof(newname), "%s.N", filename);

    if (!lstat(filename, &fileinfo) && !S_ISREG(fileinfo.st_mode))
    {
      fprintf(stderr, "ERROR: Local RSS path \"%s\" is not a file.\n", filename);
      return (1);
    }

    httpAssembleURIf(HTTP_URI_CODING_ALL, baseurl, sizeof(baseurl), "http",
                     NULL, server_name, atoi(server_port), "/rss%s", resource);
  }

 /*
  * Load the previous RSS file, if any...
  */

  load_rss(rss, filename);

  changed = cupsArrayCount(rss) == 0;

 /*
  * Localize for the user's chosen language...
  */

  language = cupsLangDefault();

 /*
  * Read events and update the RSS file until we are out of events.
  */

  for (exit_status = 0, event = NULL;;)
  {
    if (changed)
    {
     /*
      * Save the messages to the file again, uploading as needed...
      */

      if (save_rss(rss, newname, baseurl))
      {
	if (http)
	{
	 /*
          * Upload the RSS file...
	  */

          if ((status = cupsPutFile(http, resource, filename)) != HTTP_CREATED)
            fprintf(stderr, "ERROR: Unable to PUT %s from %s on port %d: %d %s\n",
	            resource, host, port, status, httpStatus(status));
	}
	else
	{
	 /*
          * Move the new RSS file over top the old one...
	  */

          if (rename(newname, filename))
            fprintf(stderr, "ERROR: Unable to rename %s to %s: %s\n",
	            newname, filename, strerror(errno));
	}

	changed = 0;
      }
    }

   /*
    * Wait up to 30 seconds for an event...
    */

    timeout.tv_sec  = 30;
    timeout.tv_usec = 0;

    FD_ZERO(&input);
    FD_SET(0, &input);

    if (select(1, &input, NULL, NULL, &timeout) < 0)
      continue;
    else if (!FD_ISSET(0, &input))
    {
      fprintf(stderr, "DEBUG: %s is bored, exiting...\n", argv[1]);
      break;
    }

   /*
    * Read the next event...
    */

    event = ippNew();
    while ((state = ippReadFile(0, event)) != IPP_DATA)
    {
      if (state <= IPP_IDLE)
        break;
    }

    if (state == IPP_ERROR)
      fputs("DEBUG: ippReadFile() returned IPP_ERROR!\n", stderr);

    if (state <= IPP_IDLE)
      break;

   /*
    * Collect the info from the event...
    */

    printer_up_time        = ippFindAttribute(event, "printer-up-time",
                                              IPP_TAG_INTEGER);
    notify_sequence_number = ippFindAttribute(event, "notify-sequence-number",
                                	      IPP_TAG_INTEGER);
    notify_printer_uri     = ippFindAttribute(event, "notify-printer-uri",
                                	      IPP_TAG_URI);
    subject                = cupsNotifySubject(language, event);
    text                   = cupsNotifyText(language, event);

    if (printer_up_time && notify_sequence_number && subject && text)
    {
     /*
      * Create a new RSS message...
      */

      if (notify_printer_uri)
      {
        httpSeparateURI(HTTP_URI_CODING_ALL,
	                notify_printer_uri->values[0].string.text,
			link_scheme, sizeof(link_scheme),
                        link_username, sizeof(link_username),
			link_host, sizeof(link_host), &link_port,
		        link_resource, sizeof(link_resource));
        httpAssembleURI(HTTP_URI_CODING_ALL, link_url, sizeof(link_url),
	                "http", link_username, link_host, link_port,
			link_resource);
      }

      msg = new_message(notify_sequence_number->values[0].integer,
                        xml_escape(subject), xml_escape(text),
			notify_printer_uri ? xml_escape(link_url) : NULL,
			printer_up_time->values[0].integer);

      if (!msg)
      {
        fprintf(stderr, "ERROR: Unable to create message: %s\n",
	        strerror(errno));
        exit_status = 1;
	break;
      }

     /*
      * Add it to the array...
      */

      cupsArrayAdd(rss, msg);

      changed = 1;

     /*
      * Trim the array as needed...
      */

      while (cupsArrayCount(rss) > max_events)
      {
        msg = cupsArrayFirst(rss);

	cupsArrayRemove(rss, msg);

	delete_message(msg);
      }
    }

    if (subject)
      free(subject);

    if (text)
      free(text);

    ippDelete(event);
    event = NULL;
  }

 /*
  * We only get here when idle or error...
  */

  ippDelete(event);

  if (http)
  {
    unlink(filename);
    httpClose(http);
  }

  return (exit_status);
}


/*
 * 'compare_rss()' - Compare two messages.
 */

static int				/* O - Result of comparison */
compare_rss(_cups_rss_t *a,		/* I - First message */
            _cups_rss_t *b)		/* I - Second message */
{
  return (a->sequence_number - b->sequence_number);
}


/*
 * 'delete_message()' - Free all memory used by a message.
 */

static void
delete_message(_cups_rss_t *msg)	/* I - RSS message */
{
  if (msg->subject)
    free(msg->subject);

  if (msg->text)
    free(msg->text);

  if (msg->link_url)
    free(msg->link_url);

  free(msg);
}


/*
 * 'load_rss()' - Load an existing RSS feed file.
 */

static void
load_rss(cups_array_t *rss,		/* I - RSS messages */
         const char   *filename)	/* I - File to load */
{
  FILE		*fp;			/* File pointer */
  char		line[4096],		/* Line from file */
		*subject,		/* Subject */
		*text,			/* Text */
		*link_url,		/* Link URL */
		*start,			/* Start of element */
		*end;			/* End of element */
  time_t	event_time;		/* Event time */
  int		sequence_number;	/* Sequence number */
  int		in_item;		/* In an item */
  _cups_rss_t	*msg;			/* New message */


  if ((fp = fopen(filename, "r")) == NULL)
  {
    if (errno != ENOENT)
      fprintf(stderr, "ERROR: Unable to open %s: %s\n", filename,
              strerror(errno));

    return;
  }

  subject         = NULL;
  text            = NULL;
  link_url        = NULL;
  event_time      = 0;
  sequence_number = 0;
  in_item         = 0;

  while (fgets(line, sizeof(line), fp))
  {
    if (strstr(line, "<item>"))
      in_item = 1;
    else if (strstr(line, "</item>") && in_item)
    {
      if (subject && text)
      {
        msg = new_message(sequence_number, subject, text, link_url,
	                  event_time);

        if (msg)
	  cupsArrayAdd(rss, msg);

      }
      else
      {
        if (subject)
	  free(subject);

	if (text)
	  free(text);

	if (link_url)
	  free(link_url);
      }

      subject         = NULL;
      text            = NULL;
      link_url        = NULL;
      event_time      = 0;
      sequence_number = 0;
      in_item         = 0;
    }
    else if (!in_item)
      continue;
    else if ((start = strstr(line, "<title>")) != NULL)
    {
      start += 7;
      if ((end = strstr(start, "</title>")) != NULL)
      {
        *end    = '\0';
	subject = strdup(start);
      }
    }
    else if ((start = strstr(line, "<description>")) != NULL)
    {
      start += 13;
      if ((end = strstr(start, "</description>")) != NULL)
      {
        *end = '\0';
	text = strdup(start);
      }
    }
    else if ((start = strstr(line, "<link>")) != NULL)
    {
      start += 6;
      if ((end = strstr(start, "</link>")) != NULL)
      {
        *end     = '\0';
	link_url = strdup(start);
      }
    }
    else if ((start = strstr(line, "<pubDate>")) != NULL)
    {
      start += 9;
      if ((end = strstr(start, "</pubDate>")) != NULL)
      {
        *end       = '\0';
	event_time = httpGetDateTime(start);
      }
    }
    else if ((start = strstr(line, "<guid>")) != NULL)
      sequence_number = atoi(start + 6);
  }

  if (subject)
    free(subject);

  if (text)
    free(text);

  if (link_url)
    free(link_url);

  fclose(fp);
}


/*
 * 'new_message()' - Create a new RSS message.
 */

static _cups_rss_t *			/* O - New message */
new_message(int    sequence_number,	/* I - notify-sequence-number */
            char   *subject,		/* I - Subject/summary */
            char   *text,		/* I - Text */
	    char   *link_url,		/* I - Link to printer */
	    time_t event_time)		/* I - Date/time of event */
{
  _cups_rss_t	*msg;			/* New message */


  if ((msg = calloc(1, sizeof(_cups_rss_t))) == NULL)
  {
#ifdef __clang_analyzer__
    // These free calls are really unnecessary (a failure here ultimately causes
    // an exit, which frees all memory much faster) but it makes Clang happy...
    free(subject);
    free(text);
    free(link_url);
#endif // __clang_analyzer__

    return (NULL);
  }

  msg->sequence_number = sequence_number;
  msg->subject         = subject;
  msg->text            = text;
  msg->link_url        = link_url;
  msg->event_time      = event_time;

  return (msg);
}


/*
 * 'password_cb()' - Return the cached password.
 */

static const char *			/* O - Cached password */
password_cb(const char *prompt)		/* I - Prompt string, unused */
{
  (void)prompt;

  return (rss_password);
}


/*
 * 'save_rss()' - Save messages to a RSS file.
 */

static int				/* O - 1 on success, 0 on failure */
save_rss(cups_array_t *rss,		/* I - RSS messages */
         const char   *filename,	/* I - File to save to */
	 const char   *baseurl)		/* I - Base URL */
{
  FILE		*fp;			/* File pointer */
  _cups_rss_t	*msg;			/* Current message */
  char		date[1024];		/* Current date */
  char		*href;			/* Escaped base URL */


  if ((fp = fopen(filename, "w")) == NULL)
  {
    fprintf(stderr, "ERROR: Unable to create %s: %s\n", filename,
            strerror(errno));
    return (0);
  }

  fchmod(fileno(fp), 0644);

  fputs("<?xml version=\"1.0\"?>\n", fp);
  fputs("<rss version=\"2.0\">\n", fp);
  fputs("  <channel>\n", fp);
  fputs("    <title>CUPS RSS Feed</title>\n", fp);

  href = xml_escape(baseurl);
  fprintf(fp, "    <link>%s</link>\n", href);
  free(href);

  fputs("    <description>CUPS RSS Feed</description>\n", fp);
  fputs("    <generator>" CUPS_SVERSION "</generator>\n", fp);
  fputs("    <ttl>1</ttl>\n", fp);

  fprintf(fp, "    <pubDate>%s</pubDate>\n",
          httpGetDateString2(time(NULL), date, sizeof(date)));

  for (msg = (_cups_rss_t *)cupsArrayLast(rss);
       msg;
       msg = (_cups_rss_t *)cupsArrayPrev(rss))
  {
    char *subject = xml_escape(msg->subject);
    char *text = xml_escape(msg->text);

    fputs("    <item>\n", fp);
    fprintf(fp, "      <title>%s</title>\n", subject);
    fprintf(fp, "      <description>%s</description>\n", text);
    if (msg->link_url)
      fprintf(fp, "      <link>%s</link>\n", msg->link_url);
    fprintf(fp, "      <pubDate>%s</pubDate>\n",
            httpGetDateString2(msg->event_time, date, sizeof(date)));
    fprintf(fp, "      <guid>%d</guid>\n", msg->sequence_number);
    fputs("    </item>\n", fp);

    free(subject);
    free(text);
  }

  fputs(" </channel>\n", fp);
  fputs("</rss>\n", fp);

  return (!fclose(fp));
}


/*
 * 'xml_escape()' - Copy a string, escaping &, <, and > as needed.
 */

static char *				/* O - Escaped string */
xml_escape(const char *s)		/* I - String to escape */
{
  char		*e,			/* Escaped string */
		*eptr;			/* Pointer into escaped string */
  const char	*sptr;			/* Pointer into string */
  size_t	bytes;			/* Bytes needed for string */


 /*
  * First figure out how many extra bytes we need...
  */

  for (bytes = 0, sptr = s; *sptr; sptr ++)
    if (*sptr == '&')
      bytes += 4;			/* &amp; */
    else if (*sptr == '<' || *sptr == '>')
      bytes += 3;			/* &lt; and &gt; */

 /*
  * If there is nothing to escape, just strdup() it...
  */

  if (bytes == 0)
    return (strdup(s));

 /*
  * Otherwise allocate memory and copy...
  */

  if ((e = malloc(bytes + 1 + strlen(s))) == NULL)
    return (NULL);

  for (eptr = e, sptr = s; *sptr; sptr ++)
    if (*sptr == '&')
    {
      *eptr++ = '&';
      *eptr++ = 'a';
      *eptr++ = 'm';
      *eptr++ = 'p';
      *eptr++ = ';';
    }
    else if (*sptr == '<')
    {
      *eptr++ = '&';
      *eptr++ = 'l';
      *eptr++ = 't';
      *eptr++ = ';';
    }
    else if (*sptr == '>')
    {
      *eptr++ = '&';
      *eptr++ = 'g';
      *eptr++ = 't';
      *eptr++ = ';';
    }
    else
      *eptr++ = *sptr;

  *eptr = '\0';

  return (e);
}
