| /* |
| * 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 *rss); |
| 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 */ |
| 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) |
| { |
| 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); |
| |
| 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) |
| 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; /* & */ |
| else if (*sptr == '<' || *sptr == '>') |
| bytes += 3; /* < and > */ |
| |
| /* |
| * 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); |
| } |