1/*
2 * "$Id: rss.c 12131 2014-08-28 23:38:16Z msweet $"
3 *
4 * RSS notifier for CUPS.
5 *
6 * Copyright 2007-2014 by Apple Inc.
7 * Copyright 2007 by Easy Software Products.
8 *
9 * These coded instructions, statements, and computer programs are the
10 * property of Apple Inc. and are protected by Federal copyright
11 * law.  Distribution and use rights are outlined in the file "LICENSE.txt"
12 * which should have been included with this file.  If this file is
13 * file is missing or damaged, see the license at "http://www.cups.org/".
14 */
15
16/*
17 * Include necessary headers...
18 */
19
20#include <cups/cups.h>
21#include <sys/stat.h>
22#include <cups/language.h>
23#include <cups/string-private.h>
24#include <cups/array.h>
25#include <sys/select.h>
26#include <cups/ipp-private.h>	/* TODO: Update so we don't need this */
27
28
29/*
30 * Structures...
31 */
32
33typedef struct _cups_rss_s		/**** RSS message data ****/
34{
35  int		sequence_number;	/* notify-sequence-number */
36  char		*subject,		/* Message subject/summary */
37		*text,			/* Message text */
38		*link_url;		/* Link to printer */
39  time_t	event_time;		/* When the event occurred */
40} _cups_rss_t;
41
42
43/*
44 * Local globals...
45 */
46
47static char		*rss_password;	/* Password for remote RSS */
48
49
50/*
51 * Local functions...
52 */
53
54static int		compare_rss(_cups_rss_t *a, _cups_rss_t *b);
55static void		delete_message(_cups_rss_t *rss);
56static void		load_rss(cups_array_t *rss, const char *filename);
57static _cups_rss_t	*new_message(int sequence_number, char *subject,
58			             char *text, char *link_url,
59				     time_t event_time);
60static const char	*password_cb(const char *prompt);
61static int		save_rss(cups_array_t *rss, const char *filename,
62			         const char *baseurl);
63static char		*xml_escape(const char *s);
64
65
66/*
67 * 'main()' - Main entry for the test notifier.
68 */
69
70int					/* O - Exit status */
71main(int  argc,				/* I - Number of command-line arguments */
72     char *argv[])			/* I - Command-line arguments */
73{
74  int		i;			/* Looping var */
75  ipp_t		*event;			/* Event from scheduler */
76  ipp_state_t	state;			/* IPP event state */
77  char		scheme[32],		/* URI scheme ("rss") */
78		username[256],		/* Username for remote RSS */
79		host[1024],		/* Hostname for remote RSS */
80		resource[1024],		/* RSS file */
81		*options;		/* Options */
82  int		port,			/* Port number for remote RSS */
83		max_events;		/* Maximum number of events */
84  http_t	*http;			/* Connection to remote server */
85  http_status_t	status;			/* HTTP GET/PUT status code */
86  char		filename[1024],		/* Local filename */
87		newname[1024];		/* filename.N */
88  cups_lang_t	*language;		/* Language information */
89  ipp_attribute_t *printer_up_time,	/* Timestamp on event */
90		*notify_sequence_number,/* Sequence number */
91		*notify_printer_uri;	/* Printer URI */
92  char		*subject,		/* Subject for notification message */
93		*text,			/* Text for notification message */
94		link_url[1024],		/* Link to printer */
95		link_scheme[32],	/* Scheme for link */
96		link_username[256],	/* Username for link */
97		link_host[1024],	/* Host for link */
98		link_resource[1024];	/* Resource for link */
99  int		link_port;		/* Link port */
100  cups_array_t	*rss;			/* RSS message array */
101  _cups_rss_t	*msg;			/* RSS message */
102  char		baseurl[1024];		/* Base URL */
103  fd_set	input;			/* Input set for select() */
104  struct timeval timeout;		/* Timeout for select() */
105  int		changed;		/* Has the RSS data changed? */
106  int		exit_status;		/* Exit status */
107
108
109  fprintf(stderr, "DEBUG: argc=%d\n", argc);
110  for (i = 0; i < argc; i ++)
111    fprintf(stderr, "DEBUG: argv[%d]=\"%s\"\n", i, argv[i]);
112
113 /*
114  * See whether we are publishing this RSS feed locally or remotely...
115  */
116
117  if (httpSeparateURI(HTTP_URI_CODING_ALL, argv[1], scheme, sizeof(scheme),
118                      username, sizeof(username), host, sizeof(host), &port,
119		      resource, sizeof(resource)) < HTTP_URI_OK)
120  {
121    fprintf(stderr, "ERROR: Bad RSS URI \"%s\"!\n", argv[1]);
122    return (1);
123  }
124
125  max_events = 20;
126
127  if ((options = strchr(resource, '?')) != NULL)
128  {
129    *options++ = '\0';
130
131    if (!strncmp(options, "max_events=", 11))
132    {
133      max_events = atoi(options + 11);
134
135      if (max_events <= 0)
136        max_events = 20;
137    }
138  }
139
140  rss = cupsArrayNew((cups_array_func_t)compare_rss, NULL);
141
142  if (host[0])
143  {
144   /*
145    * Remote feed, see if we can get the current file...
146    */
147
148    int	fd;				/* Temporary file */
149
150
151    if ((rss_password = strchr(username, ':')) != NULL)
152      *rss_password++ = '\0';
153
154    cupsSetPasswordCB(password_cb);
155    cupsSetUser(username);
156
157    if ((fd = cupsTempFd(filename, sizeof(filename))) < 0)
158    {
159      fprintf(stderr, "ERROR: Unable to create temporary file: %s\n",
160              strerror(errno));
161
162      return (1);
163    }
164
165    if ((http = httpConnect(host, port)) == NULL)
166    {
167      fprintf(stderr, "ERROR: Unable to connect to %s on port %d: %s\n",
168              host, port, strerror(errno));
169
170      close(fd);
171      unlink(filename);
172
173      return (1);
174    }
175
176    status = cupsGetFd(http, resource, fd);
177
178    close(fd);
179
180    if (status != HTTP_OK && status != HTTP_NOT_FOUND)
181    {
182      fprintf(stderr, "ERROR: Unable to GET %s from %s on port %d: %d %s\n",
183	      resource, host, port, status, httpStatus(status));
184
185      httpClose(http);
186      unlink(filename);
187
188      return (1);
189    }
190
191    strlcpy(newname, filename, sizeof(newname));
192
193    httpAssembleURI(HTTP_URI_CODING_ALL, baseurl, sizeof(baseurl), "http",
194                    NULL, host, port, resource);
195  }
196  else
197  {
198    const char	*cachedir,		/* CUPS_CACHEDIR */
199		*server_name,		/* SERVER_NAME */
200		*server_port;		/* SERVER_PORT */
201
202
203    http = NULL;
204
205    if ((cachedir = getenv("CUPS_CACHEDIR")) == NULL)
206      cachedir = CUPS_CACHEDIR;
207
208    if ((server_name = getenv("SERVER_NAME")) == NULL)
209      server_name = "localhost";
210
211    if ((server_port = getenv("SERVER_PORT")) == NULL)
212      server_port = "631";
213
214    snprintf(filename, sizeof(filename), "%s/rss%s", cachedir, resource);
215    snprintf(newname, sizeof(newname), "%s.N", filename);
216
217    httpAssembleURIf(HTTP_URI_CODING_ALL, baseurl, sizeof(baseurl), "http",
218                     NULL, server_name, atoi(server_port), "/rss%s", resource);
219  }
220
221 /*
222  * Load the previous RSS file, if any...
223  */
224
225  load_rss(rss, filename);
226
227  changed = cupsArrayCount(rss) == 0;
228
229 /*
230  * Localize for the user's chosen language...
231  */
232
233  language = cupsLangDefault();
234
235 /*
236  * Read events and update the RSS file until we are out of events.
237  */
238
239  for (exit_status = 0, event = NULL;;)
240  {
241    if (changed)
242    {
243     /*
244      * Save the messages to the file again, uploading as needed...
245      */
246
247      if (save_rss(rss, newname, baseurl))
248      {
249	if (http)
250	{
251	 /*
252          * Upload the RSS file...
253	  */
254
255          if ((status = cupsPutFile(http, resource, filename)) != HTTP_CREATED)
256            fprintf(stderr, "ERROR: Unable to PUT %s from %s on port %d: %d %s\n",
257	            resource, host, port, status, httpStatus(status));
258	}
259	else
260	{
261	 /*
262          * Move the new RSS file over top the old one...
263	  */
264
265          if (rename(newname, filename))
266            fprintf(stderr, "ERROR: Unable to rename %s to %s: %s\n",
267	            newname, filename, strerror(errno));
268	}
269
270	changed = 0;
271      }
272    }
273
274   /*
275    * Wait up to 30 seconds for an event...
276    */
277
278    timeout.tv_sec  = 30;
279    timeout.tv_usec = 0;
280
281    FD_ZERO(&input);
282    FD_SET(0, &input);
283
284    if (select(1, &input, NULL, NULL, &timeout) < 0)
285      continue;
286    else if (!FD_ISSET(0, &input))
287    {
288      fprintf(stderr, "DEBUG: %s is bored, exiting...\n", argv[1]);
289      break;
290    }
291
292   /*
293    * Read the next event...
294    */
295
296    event = ippNew();
297    while ((state = ippReadFile(0, event)) != IPP_DATA)
298    {
299      if (state <= IPP_IDLE)
300        break;
301    }
302
303    if (state == IPP_ERROR)
304      fputs("DEBUG: ippReadFile() returned IPP_ERROR!\n", stderr);
305
306    if (state <= IPP_IDLE)
307      break;
308
309   /*
310    * Collect the info from the event...
311    */
312
313    printer_up_time        = ippFindAttribute(event, "printer-up-time",
314                                              IPP_TAG_INTEGER);
315    notify_sequence_number = ippFindAttribute(event, "notify-sequence-number",
316                                	      IPP_TAG_INTEGER);
317    notify_printer_uri     = ippFindAttribute(event, "notify-printer-uri",
318                                	      IPP_TAG_URI);
319    subject                = cupsNotifySubject(language, event);
320    text                   = cupsNotifyText(language, event);
321
322    if (printer_up_time && notify_sequence_number && subject && text)
323    {
324     /*
325      * Create a new RSS message...
326      */
327
328      if (notify_printer_uri)
329      {
330        httpSeparateURI(HTTP_URI_CODING_ALL,
331	                notify_printer_uri->values[0].string.text,
332			link_scheme, sizeof(link_scheme),
333                        link_username, sizeof(link_username),
334			link_host, sizeof(link_host), &link_port,
335		        link_resource, sizeof(link_resource));
336        httpAssembleURI(HTTP_URI_CODING_ALL, link_url, sizeof(link_url),
337	                "http", link_username, link_host, link_port,
338			link_resource);
339      }
340
341      msg = new_message(notify_sequence_number->values[0].integer,
342                        xml_escape(subject), xml_escape(text),
343			notify_printer_uri ? xml_escape(link_url) : NULL,
344			printer_up_time->values[0].integer);
345
346      if (!msg)
347      {
348        fprintf(stderr, "ERROR: Unable to create message: %s\n",
349	        strerror(errno));
350        exit_status = 1;
351	break;
352      }
353
354     /*
355      * Add it to the array...
356      */
357
358      cupsArrayAdd(rss, msg);
359
360      changed = 1;
361
362     /*
363      * Trim the array as needed...
364      */
365
366      while (cupsArrayCount(rss) > max_events)
367      {
368        msg = cupsArrayFirst(rss);
369
370	cupsArrayRemove(rss, msg);
371
372	delete_message(msg);
373      }
374    }
375
376    if (subject)
377      free(subject);
378
379    if (text)
380      free(text);
381
382    ippDelete(event);
383    event = NULL;
384  }
385
386 /*
387  * We only get here when idle or error...
388  */
389
390  ippDelete(event);
391
392  if (http)
393  {
394    unlink(filename);
395    httpClose(http);
396  }
397
398  return (exit_status);
399}
400
401
402/*
403 * 'compare_rss()' - Compare two messages.
404 */
405
406static int				/* O - Result of comparison */
407compare_rss(_cups_rss_t *a,		/* I - First message */
408            _cups_rss_t *b)		/* I - Second message */
409{
410  return (a->sequence_number - b->sequence_number);
411}
412
413
414/*
415 * 'delete_message()' - Free all memory used by a message.
416 */
417
418static void
419delete_message(_cups_rss_t *msg)	/* I - RSS message */
420{
421  if (msg->subject)
422    free(msg->subject);
423
424  if (msg->text)
425    free(msg->text);
426
427  if (msg->link_url)
428    free(msg->link_url);
429
430  free(msg);
431}
432
433
434/*
435 * 'load_rss()' - Load an existing RSS feed file.
436 */
437
438static void
439load_rss(cups_array_t *rss,		/* I - RSS messages */
440         const char   *filename)	/* I - File to load */
441{
442  FILE		*fp;			/* File pointer */
443  char		line[4096],		/* Line from file */
444		*subject,		/* Subject */
445		*text,			/* Text */
446		*link_url,		/* Link URL */
447		*start,			/* Start of element */
448		*end;			/* End of element */
449  time_t	event_time;		/* Event time */
450  int		sequence_number;	/* Sequence number */
451  int		in_item;		/* In an item */
452  _cups_rss_t	*msg;			/* New message */
453
454
455  if ((fp = fopen(filename, "r")) == NULL)
456  {
457    if (errno != ENOENT)
458      fprintf(stderr, "ERROR: Unable to open %s: %s\n", filename,
459              strerror(errno));
460
461    return;
462  }
463
464  subject         = NULL;
465  text            = NULL;
466  link_url        = NULL;
467  event_time      = 0;
468  sequence_number = 0;
469  in_item         = 0;
470
471  while (fgets(line, sizeof(line), fp))
472  {
473    if (strstr(line, "<item>"))
474      in_item = 1;
475    else if (strstr(line, "</item>") && in_item)
476    {
477      if (subject && text)
478      {
479        msg = new_message(sequence_number, subject, text, link_url,
480	                  event_time);
481
482        if (msg)
483	  cupsArrayAdd(rss, msg);
484
485      }
486      else
487      {
488        if (subject)
489	  free(subject);
490
491	if (text)
492	  free(text);
493
494	if (link_url)
495	  free(link_url);
496      }
497
498      subject         = NULL;
499      text            = NULL;
500      link_url        = NULL;
501      event_time      = 0;
502      sequence_number = 0;
503      in_item         = 0;
504    }
505    else if (!in_item)
506      continue;
507    else if ((start = strstr(line, "<title>")) != NULL)
508    {
509      start += 7;
510      if ((end = strstr(start, "</title>")) != NULL)
511      {
512        *end    = '\0';
513	subject = strdup(start);
514      }
515    }
516    else if ((start = strstr(line, "<description>")) != NULL)
517    {
518      start += 13;
519      if ((end = strstr(start, "</description>")) != NULL)
520      {
521        *end = '\0';
522	text = strdup(start);
523      }
524    }
525    else if ((start = strstr(line, "<link>")) != NULL)
526    {
527      start += 6;
528      if ((end = strstr(start, "</link>")) != NULL)
529      {
530        *end     = '\0';
531	link_url = strdup(start);
532      }
533    }
534    else if ((start = strstr(line, "<pubDate>")) != NULL)
535    {
536      start += 9;
537      if ((end = strstr(start, "</pubDate>")) != NULL)
538      {
539        *end       = '\0';
540	event_time = httpGetDateTime(start);
541      }
542    }
543    else if ((start = strstr(line, "<guid>")) != NULL)
544      sequence_number = atoi(start + 6);
545  }
546
547  if (subject)
548    free(subject);
549
550  if (text)
551    free(text);
552
553  if (link_url)
554    free(link_url);
555
556  fclose(fp);
557}
558
559
560/*
561 * 'new_message()' - Create a new RSS message.
562 */
563
564static _cups_rss_t *			/* O - New message */
565new_message(int    sequence_number,	/* I - notify-sequence-number */
566            char   *subject,		/* I - Subject/summary */
567            char   *text,		/* I - Text */
568	    char   *link_url,		/* I - Link to printer */
569	    time_t event_time)		/* I - Date/time of event */
570{
571  _cups_rss_t	*msg;			/* New message */
572
573
574  if ((msg = calloc(1, sizeof(_cups_rss_t))) == NULL)
575    return (NULL);
576
577  msg->sequence_number = sequence_number;
578  msg->subject         = subject;
579  msg->text            = text;
580  msg->link_url        = link_url;
581  msg->event_time      = event_time;
582
583  return (msg);
584}
585
586
587/*
588 * 'password_cb()' - Return the cached password.
589 */
590
591static const char *			/* O - Cached password */
592password_cb(const char *prompt)		/* I - Prompt string, unused */
593{
594  (void)prompt;
595
596  return (rss_password);
597}
598
599
600/*
601 * 'save_rss()' - Save messages to a RSS file.
602 */
603
604static int				/* O - 1 on success, 0 on failure */
605save_rss(cups_array_t *rss,		/* I - RSS messages */
606         const char   *filename,	/* I - File to save to */
607	 const char   *baseurl)		/* I - Base URL */
608{
609  FILE		*fp;			/* File pointer */
610  _cups_rss_t	*msg;			/* Current message */
611  char		date[1024];		/* Current date */
612  char		*href;			/* Escaped base URL */
613
614
615  if ((fp = fopen(filename, "w")) == NULL)
616  {
617    fprintf(stderr, "ERROR: Unable to create %s: %s\n", filename,
618            strerror(errno));
619    return (0);
620  }
621
622  fchmod(fileno(fp), 0644);
623
624  fputs("<?xml version=\"1.0\"?>\n", fp);
625  fputs("<rss version=\"2.0\">\n", fp);
626  fputs("  <channel>\n", fp);
627  fputs("    <title>CUPS RSS Feed</title>\n", fp);
628
629  href = xml_escape(baseurl);
630  fprintf(fp, "    <link>%s</link>\n", href);
631  free(href);
632
633  fputs("    <description>CUPS RSS Feed</description>\n", fp);
634  fputs("    <generator>" CUPS_SVERSION "</generator>\n", fp);
635  fputs("    <ttl>1</ttl>\n", fp);
636
637  fprintf(fp, "    <pubDate>%s</pubDate>\n",
638          httpGetDateString2(time(NULL), date, sizeof(date)));
639
640  for (msg = (_cups_rss_t *)cupsArrayLast(rss);
641       msg;
642       msg = (_cups_rss_t *)cupsArrayPrev(rss))
643  {
644    fputs("    <item>\n", fp);
645    fprintf(fp, "      <title>%s</title>\n", msg->subject);
646    fprintf(fp, "      <description>%s</description>\n", msg->text);
647    if (msg->link_url)
648      fprintf(fp, "      <link>%s</link>\n", msg->link_url);
649    fprintf(fp, "      <pubDate>%s</pubDate>\n",
650            httpGetDateString2(msg->event_time, date, sizeof(date)));
651    fprintf(fp, "      <guid>%d</guid>\n", msg->sequence_number);
652    fputs("    </item>\n", fp);
653  }
654
655  fputs(" </channel>\n", fp);
656  fputs("</rss>\n", fp);
657
658  return (!fclose(fp));
659}
660
661
662/*
663 * 'xml_escape()' - Copy a string, escaping &, <, and > as needed.
664 */
665
666static char *				/* O - Escaped string */
667xml_escape(const char *s)		/* I - String to escape */
668{
669  char		*e,			/* Escaped string */
670		*eptr;			/* Pointer into escaped string */
671  const char	*sptr;			/* Pointer into string */
672  size_t	bytes;			/* Bytes needed for string */
673
674
675 /*
676  * First figure out how many extra bytes we need...
677  */
678
679  for (bytes = 0, sptr = s; *sptr; sptr ++)
680    if (*sptr == '&')
681      bytes += 4;			/* &amp; */
682    else if (*sptr == '<' || *sptr == '>')
683      bytes += 3;			/* &lt; and &gt; */
684
685 /*
686  * If there is nothing to escape, just strdup() it...
687  */
688
689  if (bytes == 0)
690    return (strdup(s));
691
692 /*
693  * Otherwise allocate memory and copy...
694  */
695
696  if ((e = malloc(bytes + 1 + strlen(s))) == NULL)
697    return (NULL);
698
699  for (eptr = e, sptr = s; *sptr; sptr ++)
700    if (*sptr == '&')
701    {
702      *eptr++ = '&';
703      *eptr++ = 'a';
704      *eptr++ = 'm';
705      *eptr++ = 'p';
706      *eptr++ = ';';
707    }
708    else if (*sptr == '<')
709    {
710      *eptr++ = '&';
711      *eptr++ = 'l';
712      *eptr++ = 't';
713      *eptr++ = ';';
714    }
715    else if (*sptr == '>')
716    {
717      *eptr++ = '&';
718      *eptr++ = 'g';
719      *eptr++ = 't';
720      *eptr++ = ';';
721    }
722    else
723      *eptr++ = *sptr;
724
725  *eptr = '\0';
726
727  return (e);
728}
729
730
731/*
732 * End of "$Id: rss.c 12131 2014-08-28 23:38:16Z msweet $".
733 */
734