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