1 // This file is part of BOINC.
2 // http://boinc.berkeley.edu
3 // Copyright (C) 2014 University of California
4 //
5 // BOINC is free software; you can redistribute it and/or modify it
6 // under the terms of the GNU Lesser General Public License
7 // as published by the Free Software Foundation,
8 // either version 3 of the License, or (at your option) any later version.
9 //
10 // BOINC is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13 // See the GNU Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public License
16 // along with BOINC.  If not, see <http://www.gnu.org/licenses/>.
17 
18 #include "cpp.h"
19 
20 #ifdef _WIN32
21 #include "boinc_win.h"
22 #else
23 #include "config.h"
24 #include <string>
25 #endif
26 
27 #ifdef _MSC_VER
28 #define snprintf _snprintf
29 #endif
30 
31 #include "parse.h"
32 #include "url.h"
33 #include "filesys.h"
34 #include "str_replace.h"
35 
36 #include "client_state.h"
37 #include "client_msgs.h"
38 #include "file_names.h"
39 #include "project.h"
40 
41 #include "cs_notice.h"
42 
43 using std::vector;
44 using std::string;
45 using std::deque;
46 
47 NOTICES notices;
48 RSS_FEEDS rss_feeds;
49 RSS_FEED_OP rss_feed_op;
50 
51 ////////////// UTILITY FUNCTIONS ///////////////
52 
cmp(NOTICE n1,NOTICE n2)53 static bool cmp(NOTICE n1, NOTICE n2) {
54     if (n1.arrival_time > n2.arrival_time) return true;
55     if (n1.arrival_time < n2.arrival_time) return false;
56     return (strcmp(n1.guid, n2.guid) > 0);
57 }
58 
project_feed_list_file_name(PROJ_AM * p,char * buf,int len)59 static void project_feed_list_file_name(PROJ_AM* p, char* buf, int len) {
60     char url[256];
61     escape_project_url(p->master_url, url);
62     snprintf(buf, len, "notices/feeds_%s.xml", url);
63 }
64 
65 // parse feed descs from scheduler reply or feed list file
66 //
parse_rss_feed_descs(XML_PARSER & xp,vector<RSS_FEED> & feeds)67 int parse_rss_feed_descs(XML_PARSER& xp, vector<RSS_FEED>& feeds) {
68     int retval;
69     while (!xp.get_tag()) {
70         if (!xp.is_tag) continue;
71         if (xp.match_tag("/rss_feeds")) return 0;
72         if (xp.match_tag("rss_feed")) {
73             RSS_FEED rf;
74             retval = rf.parse_desc(xp);
75             if (retval) {
76                 if (log_flags.sched_op_debug) {
77                     msg_printf(0, MSG_INFO,
78                         "[sched_op] error in <rss_feed> element"
79                     );
80                 }
81             } else {
82                 feeds.push_back(rf);
83             }
84         }
85     }
86     return ERR_XML_PARSE;
87 }
88 
89 // write a list of feeds to a file
90 //
write_rss_feed_descs(MIOFILE & fout,vector<RSS_FEED> & feeds)91 static void write_rss_feed_descs(MIOFILE& fout, vector<RSS_FEED>& feeds) {
92     if (!feeds.size()) return;
93     fout.printf("<rss_feeds>\n");
94     for (unsigned int i=0; i<feeds.size(); i++) {
95         feeds[i].write(fout);
96     }
97     fout.printf("</rss_feeds>\n");
98 }
99 
write_project_feed_list(PROJ_AM * p)100 static void write_project_feed_list(PROJ_AM* p) {
101     char buf[256];
102     project_feed_list_file_name(p, buf, sizeof(buf));
103     FILE* f = fopen(buf, "w");
104     if (!f) return;
105     MIOFILE fout;
106     fout.init_file(f);
107     write_rss_feed_descs(fout, p->proj_feeds);
108     fclose(f);
109 }
110 
111 // A scheduler RPC returned a list (possibly empty) of feeds.
112 // Add new ones to the project's set,
113 // and remove ones from the project's set that aren't in the list.
114 //
handle_sr_feeds(vector<RSS_FEED> & feeds,PROJ_AM * p)115 void handle_sr_feeds(vector<RSS_FEED>& feeds, PROJ_AM* p) {
116     unsigned int i, j;
117     bool feed_set_changed = false;
118 
119     // mark current feeds as not found
120     //
121     for (i=0; i<p->proj_feeds.size(); i++) {
122         p->proj_feeds[i].found = false;
123     }
124 
125     for (i=0; i<feeds.size(); i++) {
126         RSS_FEED& rf = feeds[i];
127         bool present = false;
128         for (j=0; j<p->proj_feeds.size(); j++) {
129             RSS_FEED& rf2 = p->proj_feeds[j];
130             if (!strcmp(rf.url, rf2.url)) {
131                 rf2 = rf;
132                 rf2.found = true;
133                 present = true;
134                 break;
135             }
136         }
137         if (!present) {
138             rf.found = true;
139             p->proj_feeds.push_back(rf);
140             feed_set_changed = true;
141         }
142     }
143 
144     // remove ones no longer present
145     //
146     vector<RSS_FEED>::iterator iter = p->proj_feeds.begin();
147     while (iter != p->proj_feeds.end()) {
148         RSS_FEED& rf = *iter;
149         if (rf.found) {
150             ++iter;
151         } else {
152             iter = p->proj_feeds.erase(iter);
153             feed_set_changed = true;
154         }
155     }
156 
157     // if anything was added or removed, update master set
158     //
159     if (feed_set_changed) {
160         write_project_feed_list(p);
161         rss_feeds.update_feed_list();
162     }
163 }
164 
165 #ifdef _WIN32
166 // compensate for lameness
month_index(char * x)167 static int month_index(char* x) {
168     if (strstr(x, "Jan")) return 0;
169     if (strstr(x, "Feb")) return 1;
170     if (strstr(x, "Mar")) return 2;
171     if (strstr(x, "Apr")) return 3;
172     if (strstr(x, "May")) return 4;
173     if (strstr(x, "Jun")) return 5;
174     if (strstr(x, "Jul")) return 6;
175     if (strstr(x, "Aug")) return 7;
176     if (strstr(x, "Sep")) return 8;
177     if (strstr(x, "Oct")) return 9;
178     if (strstr(x, "Nov")) return 10;
179     if (strstr(x, "Dec")) return 11;
180     return 0;
181 }
182 #endif
183 
184 // convert a date-time string (assumed GMT) to Unix time
185 
parse_rss_time(char * buf)186 static int parse_rss_time(char* buf) {
187 #ifdef _WIN32
188     char day_name[64], month_name[64];
189     int day_num, year, h, m, s;
190     sscanf(buf, "%s %d %s %d %d:%d:%d",
191         day_name, &day_num, month_name, &year, &h, &m, &s
192     );
193 
194     struct tm tm;
195     tm.tm_sec = s;
196     tm.tm_min = m;
197     tm.tm_hour = h;
198     tm.tm_mday = day_num;
199     tm.tm_mon = month_index(month_name);
200     tm.tm_year = year-1900;
201     tm.tm_wday = 0;
202     tm.tm_yday = 0;
203     tm.tm_isdst = 0;
204 
205     return (int)mktime(&tm);
206 #else
207     struct tm tm;
208     memset(&tm, 0, sizeof(tm));
209     strptime(buf, "%a, %d %b %Y %H:%M:%S", &tm);
210     return mktime(&tm);
211 #endif
212 }
213 
214 ///////////// NOTICE ////////////////
215 
parse_rss(XML_PARSER & xp)216 int NOTICE::parse_rss(XML_PARSER& xp) {
217     char buf[256];
218 
219     clear();
220     while (!xp.get_tag()) {
221         if (!xp.is_tag) continue;
222         if (xp.match_tag("/item")) return 0;
223         if (xp.parse_str("title", title, sizeof(title))) continue;
224         if (xp.parse_str("link", link, sizeof(link))) continue;
225         if (xp.parse_str("guid", guid, sizeof(guid))) continue;
226         if (xp.parse_string("description", description)) continue;
227         if (xp.parse_str("pubDate", buf, sizeof(buf))) {
228             create_time = parse_rss_time(buf);
229             continue;
230         }
231     }
232     return ERR_XML_PARSE;
233 }
234 
235 ///////////// NOTICES ////////////////
236 
237 // called at the start of client initialization
238 //
init()239 void NOTICES::init() {
240 #if 0
241     read_archive_file(NOTICES_DIR"/archive.xml", NULL);
242     if (log_flags.notice_debug) {
243         msg_printf(0, MSG_INFO, "read %d BOINC notices", (int)notices.size());
244     }
245     write_archive(NULL);
246 #endif
247 }
248 
249 // called at the end of client initialization
250 //
init_rss()251 void NOTICES::init_rss() {
252     rss_feeds.init();
253     if (log_flags.notice_debug) {
254         msg_printf(0, MSG_INFO, "read %d total notices", (int)notices.size());
255     }
256 
257     // sort by decreasing arrival time, then assign seqnos
258     //
259     sort(notices.begin(), notices.end(), cmp);
260     size_t n = notices.size();
261     for (unsigned int i=0; i<n; i++) {
262         notices[i].seqno = (int)(n - i);
263     }
264 }
265 
266 // return true if strings are the same after discarding digits.
267 // This eliminates showing
268 // "you need 25 GB more disk space" and
269 // "you need 24 GB more disk space" as separate notices.
270 //
string_equal_nodigits(string & s1,string & s2)271 static inline bool string_equal_nodigits(string& s1, string& s2) {
272     const char *p = s1.c_str();
273     const char *q = s2.c_str();
274     while (1) {
275         if (isascii(*p) && isdigit(*p)) {
276             p++;
277             continue;
278         }
279         if (isascii(*q) && isdigit(*q)) {
280             q++;
281             continue;
282         }
283         if (!*p || !*q) break;
284         if (*p != *q) return false;
285         p++;
286         q++;
287     }
288     if (*p || *q) return false;
289     return true;
290 }
291 
same_text(NOTICE & n1,NOTICE & n2)292 static inline bool same_text(NOTICE& n1, NOTICE& n2) {
293     if (strcmp(n1.title, n2.title)) {
294         return false;
295     }
296     if (!string_equal_nodigits(n1.description, n2.description)) {
297         return false;
298     }
299     return true;
300 }
301 
clear_keep()302 void NOTICES::clear_keep() {
303     deque<NOTICE>::iterator i = notices.begin();
304     while (i != notices.end()) {
305         NOTICE& n = *i;
306         n.keep = false;
307         ++i;
308     }
309 }
310 
unkeep(const char * url)311 void NOTICES::unkeep(const char* url) {
312     deque<NOTICE>::iterator i = notices.begin();
313     bool removed_something = false;
314     while (i != notices.end()) {
315         NOTICE& n = *i;
316         if (!strcmp(url, n.feed_url) && !n.keep) {
317             i = notices.erase(i);
318             removed_something = true;
319         } else {
320             ++i;
321         }
322     }
323 #ifndef SIM
324     if (removed_something) {
325         gstate.gui_rpcs.set_notice_refresh();
326     }
327 #endif
328 }
329 
330 #if 0
331 static inline bool same_guid(NOTICE& n1, NOTICE& n2) {
332     if (!strlen(n1.guid)) return false;
333     return !strcmp(n1.guid, n2.guid);
334 }
335 #endif
336 
337 // we're considering adding a notice n.
338 // If there's already an identical message n2
339 //     return false (don't add n)
340 // If there's a message n2 with same title and text,
341 //      and n is significantly newer than n2,
342 //      delete n2
343 //
344 // Also remove notices older than 30 days
345 //
remove_dups(NOTICE & n)346 bool NOTICES::remove_dups(NOTICE& n) {
347     deque<NOTICE>::iterator i = notices.begin();
348     bool removed_something = false;
349     bool retval = true;
350     double min_time = gstate.now - 30*86400;
351     while (i != notices.end()) {
352         NOTICE& n2 = *i;
353 
354         if (log_flags.notice_debug) {
355             msg_printf(0, MSG_INFO,
356                 "[notice] scanning old notice %d: %s",
357                 n2.seqno, strlen(n2.title)?n2.title:n2.description.c_str()
358             );
359         }
360         if (n2.arrival_time < min_time
361             || (n2.create_time && n2.create_time < min_time)
362         ) {
363             i = notices.erase(i);
364             removed_something = true;
365             if (log_flags.notice_debug) {
366                 msg_printf(0, MSG_INFO,
367                     "[notice] removing old notice %d: %s",
368                     n2.seqno, strlen(n2.title)?n2.title:n2.description.c_str()
369                 );
370             }
371 #if 0
372         // this check prevents news item edits from showing; skip it
373         } else if (same_guid(n, n2)) {
374             n2.keep = true;
375             return false;
376 #endif
377         } else if (same_text(n, n2)) {
378             int min_diff = 0;
379 
380             // show a given scheduler notice at most once a week
381             //
382             if (!strcmp(n.category, "scheduler")) {
383                 min_diff = 7*86400;
384             }
385 
386             if (n.create_time > n2.create_time + min_diff) {
387                 i = notices.erase(i);
388                 removed_something = true;
389                 if (log_flags.notice_debug) {
390                     msg_printf(0, MSG_INFO,
391                         "[notice] replacing identical older notice %d", n2.seqno
392                     );
393                 }
394             } else {
395                 n2.keep = true;
396                 retval = false;
397                 ++i;
398                 if (log_flags.notice_debug) {
399                     msg_printf(0, MSG_INFO,
400                         "[notice] keeping identical older notice %d", n2.seqno
401                     );
402                 }
403             }
404         } else {
405             ++i;
406         }
407     }
408 #ifndef SIM
409     if (removed_something) {
410         gstate.gui_rpcs.set_notice_refresh();
411     }
412 #endif
413     return retval;
414 }
415 
416 // add a notice.
417 //
append(NOTICE & n)418 bool NOTICES::append(NOTICE& n) {
419     if (log_flags.notice_debug) {
420         msg_printf(0, MSG_INFO,
421             "[notice] processing notice: %s",
422             strlen(n.title)?n.title:n.description.c_str()
423         );
424     }
425     if (!remove_dups(n)) {
426         return false;
427     }
428     if (notices.empty()) {
429         n.seqno = 1;
430     } else {
431         n.seqno = notices.front().seqno + 1;
432     }
433     if (log_flags.notice_debug) {
434         msg_printf(0, MSG_INFO,
435             "[notice] adding notice %d", n.seqno
436         );
437     }
438     notices.push_front(n);
439 #if 0
440     if (!strlen(n.feed_url)) {
441         write_archive(NULL);
442     }
443 #endif
444     return true;
445 }
446 
447 
448 // read and parse the contents of an archive file.
449 // If rfp is NULL it's a system msg, else a feed msg.
450 // insert items in NOTICES
451 //
read_archive_file(const char * path,RSS_FEED * rfp)452 int NOTICES::read_archive_file(const char* path, RSS_FEED* rfp) {
453     FILE* f = fopen(path, "r");
454     if (!f) {
455         if (log_flags.notice_debug) {
456             msg_printf(0, MSG_INFO,
457                 "[notice] no archive file %s", path
458             );
459         }
460         return 0;
461     }
462     MIOFILE fin;
463     fin.init_file(f);
464     XML_PARSER xp(&fin);
465     while (!xp.get_tag()) {
466         if (!xp.is_tag) continue;
467         if (xp.match_tag("/notices")) {
468             fclose(f);
469             return 0;
470         }
471         if (xp.match_tag("notice")) {
472             NOTICE n;
473             int retval = n.parse(xp);
474             if (retval) {
475                 if (log_flags.notice_debug) {
476                     msg_printf(0, MSG_INFO,
477                         "[notice] archive item parse error: %d", retval
478                     );
479                 }
480             } else {
481                 if (rfp) {
482                     safe_strcpy(n.feed_url, rfp->url);
483                     safe_strcpy(n.project_name, rfp->project_name);
484                 }
485                 append(n);
486             }
487         }
488     }
489     if (log_flags.notice_debug) {
490         msg_printf(0, MSG_INFO, "[notice] archive parse error");
491     }
492     fclose(f);
493     return ERR_XML_PARSE;
494 }
495 
496 // write archive file for the given RSS feed
497 // (or, if NULL, non-RSS notices)
498 //
write_archive(RSS_FEED * rfp)499 void NOTICES::write_archive(RSS_FEED* rfp) {
500     char path[MAXPATHLEN];
501 
502     if (rfp) {
503         rfp->archive_file_name(path, sizeof(path));
504     } else {
505         safe_strcpy(path, NOTICES_DIR"/archive.xml");
506     }
507     FILE* f = fopen(path, "w");
508     if (!f) return;
509     MIOFILE fout;
510     fout.init_file(f);
511     fout.printf("<notices>\n");
512     if (!f) return;
513     for (unsigned int i=0; i<notices.size(); i++) {
514         NOTICE& n = notices[i];
515         if (rfp) {
516             if (strcmp(rfp->url, n.feed_url)) continue;
517         } else {
518             if (strlen(n.feed_url)) continue;
519         }
520         n.write(fout, false);
521     }
522     fout.printf("</notices>\n");
523     fclose(f);
524 }
525 
526 // Remove outdated notices
527 //
remove_notices(PROJECT * p,int which)528 void NOTICES::remove_notices(PROJECT* p, int which) {
529     deque<NOTICE>::iterator i = notices.begin();
530     while (i != notices.end()) {
531         NOTICE& n = *i;
532         if (p && strcmp(n.project_name, p->get_project_name())) {
533             ++i;
534             continue;
535         }
536         bool remove = false;
537         switch (which) {
538         case REMOVE_NETWORK_MSG:
539             remove = !strcmp(n.description.c_str(), NEED_NETWORK_MSG);
540             break;
541         case REMOVE_SCHEDULER_MSG:
542             remove = !strcmp(n.category, "scheduler");
543             break;
544         case REMOVE_NO_WORK_MSG:
545             remove = !strcmp(n.description.c_str(), NO_WORK_MSG);
546             break;
547         case REMOVE_CONFIG_MSG:
548             remove = (strstr(n.description.c_str(), "cc_config.xml") != NULL);
549             break;
550         case REMOVE_APP_INFO_MSG:
551             remove = (strstr(n.description.c_str(), "app_info.xml") != NULL);
552             break;
553         case REMOVE_APP_CONFIG_MSG:
554             remove = (strstr(n.description.c_str(), "app_config.xml") != NULL);
555             break;
556         }
557         if (remove) {
558             i = notices.erase(i);
559 #ifndef SIM
560             gstate.gui_rpcs.set_notice_refresh();
561 #endif
562             if (log_flags.notice_debug) {
563                 msg_printf(p, MSG_INFO, "Removing notices of type %d", which);
564             }
565         } else {
566             ++i;
567         }
568     }
569 }
570 
571 // write notices newer than seqno as XML (for GUI RPC).
572 // Write them in order of increasing seqno
573 //
write(int seqno,GUI_RPC_CONN & grc,bool public_only)574 void NOTICES::write(int seqno, GUI_RPC_CONN& grc, bool public_only) {
575     size_t i;
576     MIOFILE mf;
577 
578     if (!net_status.need_physical_connection) {
579         remove_notices(NULL, REMOVE_NETWORK_MSG);
580     }
581     if (log_flags.notice_debug) {
582         msg_printf(0, MSG_INFO, "NOTICES::write: seqno %d, refresh %s, %d notices",
583             seqno, grc.get_notice_refresh()?"true":"false", (int)notices.size()
584         );
585     }
586     grc.mfout.printf("<notices>\n");
587     if (grc.get_notice_refresh()) {
588         grc.clear_notice_refresh();
589         NOTICE n;
590         n.seqno = -1;
591         seqno = -1;
592         i = notices.size();
593         n.write(grc.mfout, true);
594         if (log_flags.notice_debug) {
595             msg_printf(0, MSG_INFO, "NOTICES::write: sending -1 seqno notice");
596         }
597     } else {
598         for (i=0; i<notices.size(); i++) {
599             NOTICE& n = notices[i];
600             if (n.seqno <= seqno) break;
601         }
602     }
603     for (; i>0; i--) {
604         NOTICE& n = notices[i-1];
605         if (public_only && n.is_private) continue;
606         if (log_flags.notice_debug) {
607             msg_printf(0, MSG_INFO, "NOTICES::write: sending notice %d", n.seqno);
608         }
609         n.write(grc.mfout, true);
610     }
611     grc.mfout.printf("</notices>\n");
612 }
613 
614 ///////////// RSS_FEED ////////////////
615 
feed_file_name(char * path,int len)616 void RSS_FEED::feed_file_name(char* path, int len) {
617     char buf[256];
618     escape_project_url(url_base, buf);
619     snprintf(path, len, NOTICES_DIR"/%s.xml", buf);
620 }
621 
archive_file_name(char * path,int len)622 void RSS_FEED::archive_file_name(char* path, int len) {
623     char buf[256];
624     escape_project_url(url_base, buf);
625     snprintf(path, len, NOTICES_DIR"/archive_%s.xml", buf);
626 }
627 
628 // read and parse the contents of the archive file;
629 // insert items in NOTICES
630 //
read_archive_file()631 int RSS_FEED::read_archive_file() {
632     char path[MAXPATHLEN];
633     archive_file_name(path, sizeof(path));
634     return notices.read_archive_file(path, this);
635 }
636 
637 // parse a feed descriptor (in scheduler reply or feed list file)
638 //
parse_desc(XML_PARSER & xp)639 int RSS_FEED::parse_desc(XML_PARSER& xp) {
640     safe_strcpy(url, "");
641     poll_interval = 0;
642     next_poll_time = 0;
643     while (!xp.get_tag()) {
644         if (!xp.is_tag) continue;
645         if (xp.match_tag("/rss_feed")) {
646             if (!poll_interval || !strlen(url)) {
647                 if (log_flags.notice_debug) {
648                     msg_printf(0, MSG_INFO,
649                         "[notice] URL or poll interval missing in sched reply feed"
650                     );
651                 }
652                 return ERR_XML_PARSE;
653             }
654             safe_strcpy(url_base, url);
655             char* p = strchr(url_base, '?');
656             if (p) *p = 0;
657             return 0;
658         }
659         if (xp.parse_str("url", url, sizeof(url))) {
660             xml_unescape(url);
661         }
662         if (xp.parse_double("poll_interval", poll_interval)) continue;
663         if (xp.parse_double("next_poll_time", next_poll_time)) continue;
664     }
665     return ERR_XML_PARSE;
666 }
667 
write(MIOFILE & fout)668 void RSS_FEED::write(MIOFILE& fout) {
669     char buf[256];
670     safe_strcpy(buf, url);
671     xml_escape(url, buf, sizeof(buf));
672     fout.printf(
673         "  <rss_feed>\n"
674         "    <url>%s</url>\n"
675         "    <poll_interval>%f</poll_interval>\n"
676         "    <next_poll_time>%f</next_poll_time>\n"
677         "  </rss_feed>\n",
678         buf,
679         poll_interval,
680         next_poll_time
681     );
682 }
683 
create_time_asc(NOTICE n1,NOTICE n2)684 static inline bool create_time_asc(NOTICE n1, NOTICE n2) {
685     return n1.create_time < n2.create_time;
686 }
687 
688 // parse the actual RSS feed.
689 //
parse_items(XML_PARSER & xp,int & nitems)690 int RSS_FEED::parse_items(XML_PARSER& xp, int& nitems) {
691     nitems = 0;
692     int ntotal = 0, nerror = 0;
693     int retval, func_ret = ERR_XML_PARSE;
694     vector<NOTICE> new_notices;
695 
696     notices.clear_keep();
697 
698     while (!xp.get_tag()) {
699         if (!xp.is_tag) continue;
700         if (xp.match_tag("/rss")) {
701             if (log_flags.notice_debug) {
702                 msg_printf(0, MSG_INFO,
703                     "[notice] parsed RSS feed: total %d error %d added %d",
704                     ntotal, nerror, nitems
705                 );
706             }
707             func_ret = 0;
708             break;
709         }
710         if (xp.match_tag("item")) {
711             NOTICE n;
712             ntotal++;
713             retval = n.parse_rss(xp);
714             if (retval) {
715                 nerror++;
716             } else if (n.create_time < gstate.now - 30*86400) {
717                 if (log_flags.notice_debug) {
718                     msg_printf(0, MSG_INFO,
719                         "[notice] item is older than 30 days: %s",
720                         n.title
721                     );
722                 }
723             } else {
724                 n.arrival_time = gstate.now;
725                 n.keep = true;
726                 safe_strcpy(n.feed_url, url);
727                 safe_strcpy(n.project_name, project_name);
728                 new_notices.push_back(n);
729             }
730             continue;
731         }
732         if (xp.parse_int("error_num", retval)) {
733             if (log_flags.notice_debug) {
734                 msg_printf(0,MSG_INFO,
735                     "[notice] RSS fetch returned error %d (%s)",
736                     retval,
737                     boincerror(retval)
738                 );
739             }
740             return retval;
741         }
742     }
743 
744     //  sort new notices by increasing create time, and append them
745     //
746     std::sort(new_notices.begin(), new_notices.end(), create_time_asc);
747     for (unsigned int i=0; i<new_notices.size(); i++) {
748         NOTICE& n = new_notices[i];
749         if (notices.append(n)) {
750             nitems++;
751         }
752     }
753     notices.unkeep(url);
754     return func_ret;
755 }
756 
delete_files()757 void RSS_FEED::delete_files() {
758     char path[MAXPATHLEN];
759     feed_file_name(path, sizeof(path));
760     boinc_delete_file(path);
761     archive_file_name(path, sizeof(path));
762     boinc_delete_file(path);
763 }
764 
765 ///////////// RSS_FEED_OP ////////////////
766 
RSS_FEED_OP()767 RSS_FEED_OP::RSS_FEED_OP() {
768     error_num = BOINC_SUCCESS;
769     rfp = NULL;
770     gui_http = &gstate.gui_http;
771 }
772 
773 // see if time to start new fetch
774 //
poll()775 bool RSS_FEED_OP::poll() {
776     unsigned int i;
777     char file_name[256];
778     if (gstate.gui_http.is_busy()) return false;
779     if (gstate.network_suspended) return false;
780     for (i=0; i<rss_feeds.feeds.size(); i++) {
781         RSS_FEED& rf = rss_feeds.feeds[i];
782         if (gstate.now > rf.next_poll_time) {
783             rf.next_poll_time = gstate.now + rf.poll_interval;
784             rf.feed_file_name(file_name, sizeof(file_name));
785             rfp = &rf;
786             if (log_flags.notice_debug) {
787                 msg_printf(0, MSG_INFO,
788                     "[notice] start fetch from %s", rf.url
789                 );
790             }
791             char url[1024];
792             safe_strcpy(url, rf.url);
793             gstate.gui_http.do_rpc(this, url, file_name, true);
794             break;
795         }
796     }
797     return false;
798 }
799 
800 // handle a completed RSS feed fetch
801 //
handle_reply(int http_op_retval)802 void RSS_FEED_OP::handle_reply(int http_op_retval) {
803     char file_name[256];
804     int nitems;
805 
806     if (!rfp) return;   // op was canceled
807 
808     if (http_op_retval) {
809         if (log_flags.notice_debug) {
810             msg_printf(0, MSG_INFO,
811                 "[notice] fetch of %s failed: %d", rfp->url, http_op_retval
812             );
813         }
814         return;
815     }
816 
817     if (log_flags.notice_debug) {
818         msg_printf(0, MSG_INFO,
819             "[notice] handling reply from %s", rfp->url
820         );
821     }
822 
823     rfp->feed_file_name(file_name, sizeof(file_name));
824     FILE* f = fopen(file_name, "r");
825     if (!f) {
826         msg_printf(0, MSG_INTERNAL_ERROR,
827             "RSS feed file '%s' not found", file_name
828         );
829         return;
830     }
831     MIOFILE fin;
832     fin.init_file(f);
833     XML_PARSER xp(&fin);
834     int retval = rfp->parse_items(xp, nitems);
835     if (retval) {
836         if (log_flags.notice_debug) {
837             msg_printf(0, MSG_INFO,
838                 "[notice] RSS parse error: %d", retval
839             );
840         }
841     }
842     fclose(f);
843 
844     notices.write_archive(rfp);
845 }
846 
847 ///////////// RSS_FEEDS ////////////////
848 
init_proj_am(PROJ_AM * p)849 static void init_proj_am(PROJ_AM* p) {
850     FILE* f;
851     MIOFILE fin;
852     char path[MAXPATHLEN];
853 
854     project_feed_list_file_name(p, path, sizeof(path));
855     f = fopen(path, "r");
856     if (f) {
857         fin.init_file(f);
858         XML_PARSER xp(&fin);
859         parse_rss_feed_descs(xp, p->proj_feeds);
860         fclose(f);
861     }
862 }
863 
864 // called on startup.  Get list of feeds.  Read archives.
865 //
init()866 void RSS_FEEDS::init() {
867     unsigned int i;
868 
869     boinc_mkdir(NOTICES_DIR);
870 
871     if (!gstate.acct_mgr_info.get_no_project_notices()) {
872         for (i=0; i<gstate.projects.size(); i++) {
873             PROJECT* p = gstate.projects[i];
874             init_proj_am(p);
875         }
876     }
877     if (gstate.acct_mgr_info.using_am()) {
878         init_proj_am(&gstate.acct_mgr_info);
879     }
880 
881     update_feed_list();
882 
883     for (i=0; i<feeds.size(); i++) {
884         RSS_FEED& rf = feeds[i];
885         if (log_flags.notice_debug) {
886             msg_printf(0, MSG_INFO,
887                 "[notice] feed: %s, %.0f sec",
888                 rf.url, rf.poll_interval
889             );
890         }
891         rf.read_archive_file();
892     }
893 }
894 
lookup_url(char * url)895 RSS_FEED* RSS_FEEDS::lookup_url(char* url) {
896     for (unsigned int i=0; i<feeds.size(); i++) {
897         RSS_FEED& rf = feeds[i];
898         if (!strcmp(rf.url, url)) {
899             return &rf;
900         }
901     }
902     return NULL;
903 }
904 
905 // arrange to fetch the project's feeds
906 //
trigger_fetch(PROJ_AM * p)907 void RSS_FEEDS::trigger_fetch(PROJ_AM* p) {
908     for (unsigned int i=0; i<p->proj_feeds.size(); i++) {
909         RSS_FEED& rf = p->proj_feeds[i];
910         RSS_FEED* rfp = lookup_url(rf.url);
911         if (rfp) {
912             rfp->next_poll_time = 0;
913         }
914     }
915 }
916 
update_proj_am(PROJ_AM * p)917 void RSS_FEEDS::update_proj_am(PROJ_AM* p) {
918     unsigned int j;
919     for (j=0; j<p->proj_feeds.size(); j++) {
920         RSS_FEED& rf = p->proj_feeds[j];
921         RSS_FEED* rfp = lookup_url(rf.url);
922         if (rfp) {
923             rfp->found = true;
924         } else {
925             rf.found = true;
926             safe_strcpy(rf.project_name, p->get_project_name());
927             feeds.push_back(rf);
928             if (log_flags.notice_debug) {
929                 msg_printf(0, MSG_INFO,
930                     "[notice] adding feed: %s, %.0f sec",
931                     rf.url, rf.poll_interval
932                 );
933             }
934         }
935     }
936 }
937 
938 // the set of project feeds has changed.
939 // update the master list.
940 //
update_feed_list()941 void RSS_FEEDS::update_feed_list() {
942     unsigned int i;
943     for (i=0; i<feeds.size(); i++) {
944         RSS_FEED& rf = feeds[i];
945         rf.found = false;
946     }
947     if (!gstate.acct_mgr_info.get_no_project_notices()) {
948         for (i=0; i<gstate.projects.size(); i++) {
949             PROJECT* p = gstate.projects[i];
950             update_proj_am(p);
951         }
952     }
953     if (gstate.acct_mgr_info.using_am()) {
954         update_proj_am(&gstate.acct_mgr_info);
955     }
956     vector<RSS_FEED>::iterator iter = feeds.begin();
957     while (iter != feeds.end()) {
958         RSS_FEED& rf = *iter;
959         if (rf.found) {
960             ++iter;
961         } else {
962             // cancel op if active
963             //
964             if (rss_feed_op.rfp == &(*iter)) {
965                 if (rss_feed_op.gui_http->is_busy()) {
966                     gstate.http_ops->remove(&rss_feed_op.gui_http->http_op);
967                 }
968                 rss_feed_op.rfp = NULL;
969             }
970             if (log_flags.notice_debug) {
971                 msg_printf(0, MSG_INFO,
972                     "[notice] removing feed: %s",
973                     rf.url
974                 );
975             }
976             rf.delete_files();
977             iter = feeds.erase(iter);
978         }
979     }
980     write_feed_list();
981 }
982 
write_feed_list()983 void RSS_FEEDS::write_feed_list() {
984     FILE* f = fopen(NOTICES_DIR"/feeds.xml", "w");
985     if (!f) return;
986     MIOFILE fout;
987     fout.init_file(f);
988     write_rss_feed_descs(fout, feeds);
989     fclose(f);
990 }
991 
delete_project_notice_files(PROJECT * p)992 void delete_project_notice_files(PROJECT* p) {
993     char path[MAXPATHLEN];
994     project_feed_list_file_name(p, path, sizeof(path));
995     boinc_delete_file(path);
996 }
997