1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /*
3  * Pan - A Newsreader for Gtk+
4  * Copyright (C) 2002-2006  Charles Kerr <charles@rebelbase.com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; version 2 of the License.
9  *
10  * This program 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.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, see <http://www.gnu.org/licenses/>.
17  *
18  */
19 
20 /**************
21 ***************
22 **************/
23 
24 #include <config.h>
25 #include <cassert>
26 #include <iostream>
27 #include <map>
28 #include <set>
29 #include <vector>
30 
31 #include <glib.h>
32 extern "C" {
33   #include <unistd.h>
34   #include <glib/gi18n.h>
35 }
36 
37 #include <pan/general/debug.h>
38 #include <pan/general/file-util.h>
39 #include <pan/general/log.h>
40 #include <pan/general/macros.h>
41 #include <pan/general/messages.h>
42 #include <pan/general/time-elapsed.h>
43 
44 #include <pan/usenet-utils/numbers.h>
45 
46 #include "data-impl.h"
47 
48 using namespace pan;
49 
50 /**
51 ***
52 **/
53 
54 namespace
55 {
56   bool
parse_newsrc_line(const StringView & line,StringView & setme_group_name,bool & setme_subscribed,StringView & setme_number_ranges)57   parse_newsrc_line (const StringView  & line,
58                      StringView        & setme_group_name,
59                      bool              & setme_subscribed,
60                      StringView        & setme_number_ranges)
61   {
62     bool ok (false);
63     // since most groups will be unread, it's faster to test the end of the line before calling strpbrk
64     const char * delimiter ((!line.empty() && (line.back()=='!' || line.back()==':')) ? &line.back() : line.strpbrk("!:"));
65     if (delimiter)
66     {
67       StringView myline (line);
68 
69       setme_subscribed = *delimiter == ':';
70 
71       myline.substr (NULL, delimiter, setme_group_name);
72       setme_group_name.trim ();
73 
74       myline.substr (delimiter+1, NULL, setme_number_ranges);
75       setme_number_ranges.trim ();
76 
77       ok = !setme_group_name.empty();
78     }
79 
80     return ok;
81   }
82 }
83 
84 // detect std::lib
85 #include <ciso646>
86 #ifdef _LIBCPP_VERSION
87 // using libc++
88 #include <algorithm>
89 #else
90 // using libstdc++
91 #include <ext/algorithm>
92 #endif
93 
94 void
load_newsrc(const Quark & server,LineReader * in,alpha_groups_t & sub,alpha_groups_t & unsub)95 DataImpl :: load_newsrc (const Quark       & server,
96                          LineReader        * in,
97                          alpha_groups_t    & sub,
98                          alpha_groups_t    & unsub)
99 {
100   Server * s = find_server (server);
101   if (!s) {
102     Log::add_err_va (_("Skipping newsrc file for server “%s”"), server.c_str());
103     return;
104   }
105 
106   std::vector<Quark>& groups (s->groups.get_container());
107 
108   AlphabeticalQuarkOrdering o;
109   StringView line, name, numbers;
110   bool needs_sort (false);
111   Quark prev;
112   std::vector<Quark> tmp_sub, tmp_unsub;
113   tmp_sub.reserve (1000);
114   tmp_unsub.reserve (120000); // giganews has ~100k; anyone have more?
115   while (!in->fail() && in->getline (line))
116   {
117     bool subscribed;
118     if (parse_newsrc_line (line, name, subscribed, numbers))
119     {
120       const Quark& group (name);
121 
122       needs_sort |= (!prev.empty() && !o(prev,group));
123       groups.push_back (group);
124 
125       if (subscribed)
126         tmp_sub.push_back (group);
127       else
128         tmp_unsub.push_back (group);
129 
130       if (!numbers.empty())
131         _read_groups[group][server]._read.mark_str (numbers);
132 
133       prev = group;
134     }
135   }
136 
137   // sub += tmp_sub
138   if (needs_sort)
139     std::sort (tmp_sub.begin(), tmp_sub.end(), AlphabeticalQuarkOrdering());
140   if (sub.empty())
141     sub.get_container().swap (tmp_sub);
142   else {
143     std::vector<Quark> tmp;
144     tmp.reserve (sub.size() + tmp_sub.size());
145     std::set_union (sub.begin(), sub.end(),
146                     tmp_sub.begin(), tmp_sub.end(),
147                     std::inserter (tmp, tmp.begin()), o);
148     sub.get_container().swap (tmp);
149   }
150 
151   // unsub += tmp_unsub
152   if (needs_sort)
153     std::sort (tmp_unsub.begin(), tmp_unsub.end(), AlphabeticalQuarkOrdering());
154   if (unsub.empty())
155     unsub.get_container().swap (tmp_unsub);
156   else {
157     std::vector<Quark> tmp;
158     tmp.reserve (unsub.size() + tmp_unsub.size());
159     std::set_union (unsub.begin(), unsub.end(),
160                     tmp_unsub.begin(), tmp_unsub.end(),
161                     std::inserter (tmp, tmp.begin()), o);
162     unsub.get_container().swap (tmp);
163   }
164 }
165 
166 void
load_newsrc_files(const DataIO & data_io)167 DataImpl :: load_newsrc_files (const DataIO& data_io)
168 {
169   alpha_groups_t& s(_subscribed);
170   alpha_groups_t& u(_unsubscribed);
171 
172   s.clear ();
173   u.clear ();
174 
175   foreach_const (servers_t, _servers, sit) {
176     const Quark key (sit->first);
177     const std::string filename = file::absolute_fn("", sit->second.newsrc_filename);
178     if (file::file_exists (filename.c_str())) {
179       LineReader * in (data_io.read_file (filename));
180       load_newsrc (key, in, s, u);
181       delete in;
182     }
183   }
184 
185   // remove duplicates
186   s.erase (std::unique(s.begin(), s.end()), s.end());
187   u.erase (std::unique(u.begin(), u.end()), u.end());
188 
189   // unsub -= sub
190   AlphabeticalQuarkOrdering o;
191   std::vector<Quark> tmp;
192   tmp.reserve (u.size());
193   std::set_difference (u.begin(), u.end(), s.begin(), s.end(), inserter (tmp, tmp.begin()), o);
194   u.get_container().swap (tmp);
195 
196   // shrink-to-fit
197   alpha_groups_t (s).swap(s);
198   alpha_groups_t (u).swap(u);
199   fire_grouplist_rebuilt ();
200 }
201 
202 void
save_newsrc_files(DataIO & data_io) const203 DataImpl :: save_newsrc_files (DataIO& data_io) const
204 {
205   if (newsrc_autosave_id) {
206     g_source_remove( newsrc_autosave_id );
207     newsrc_autosave_id = 0;
208   }
209 
210   if (_unit_test)
211     return;
212 
213   // overly-complex optimization: both sit->second.groups and _subscribed
214   // are both ordered by AlphabeticalQuarkOrdering.
215   // Where N==sit->second.groups.size() and M==_subscribed.size(),
216   // "bool subscribed = _subscribed.count (group)" is N*log(M),
217   // but a sorted set comparison is M+N comparisons.
218   AlphabeticalQuarkOrdering o;
219 
220   // save all the servers' newsrc files
221   foreach_const (servers_t, _servers, sit)
222   {
223     const Quark& server (sit->first);
224 
225     // write this server's newsrc
226     const std::string filename = file::absolute_fn("", sit->second.newsrc_filename);
227 
228     std::ostream& out (*data_io.write_file (filename));
229     std::string newsrc_string;
230     alpha_groups_t::const_iterator sub_it (_subscribed.begin());
231     const alpha_groups_t::const_iterator sub_end(_subscribed.end());
232     foreach_const (Server::groups_t, sit->second.groups, git) // for the groups in this server...
233     {
234       const Quark& group (*git);
235 
236       //const bool subscribed (_subscribed.count (group));
237       while (sub_it!=sub_end && o (*sub_it, group)) ++sub_it; // see comment for 'o' above
238       const bool subscribed (sub_it!=sub_end && *sub_it==group);
239       out << group;
240       out.put (subscribed ? ':' : '!');
241 
242       // if the group's been read, save its read number ranges...
243       const ReadGroup::Server * rgs (find_read_group_server (group, server));
244       if (rgs != 0) {
245         newsrc_string.clear ();
246         rgs->_read.to_string (newsrc_string);
247         if (!newsrc_string.empty()) {
248           out.put (' ');
249           out << newsrc_string;
250         }
251       }
252 
253       out.put ('\n');
254     }
255     data_io.write_done (&out);
256   }
257 }
258 
259 /***
260 ****
261 ***/
262 
263 void
load_group_permissions(const DataIO & data_io)264 DataImpl :: load_group_permissions (const DataIO& data_io)
265 {
266   std::vector<Quark> m, n;
267 
268   LineReader * in (data_io.read_group_permissions ());
269   StringView s, line;
270   while (in && !in->fail() && in->getline(line))
271   {
272     if (line.len && *line.str=='#')
273       continue;
274 
275     else if (!line.pop_last_token (s, ':') || !s.len || (*s.str!='y' && *s.str!='n' && *s.str!='m')) {
276       std::cerr << LINE_ID << " Group permissions: Can't parse line `" << line << std::endl;
277       continue;
278     }
279 
280     const Quark group (line);
281     const char ch = *s.str;
282 
283     if (ch == 'm')
284       m.push_back (group);
285     else if (ch == 'n')
286       n.push_back (group);
287   }
288 
289   std::sort (m.begin(), m.end());
290   m.erase (std::unique(m.begin(), m.end()), m.end());
291   _moderated.get_container().swap (m);
292 
293   std::sort (n.begin(), n.end());
294   n.erase (std::unique(n.begin(), n.end()), n.end());
295   _nopost.get_container().swap (n);
296 
297   delete in;
298 }
299 
300 void
save_group_permissions(DataIO & data_io) const301 DataImpl :: save_group_permissions (DataIO& data_io) const
302 {
303   if (_unit_test)
304     return;
305 
306   std::ostream& out (*data_io.write_group_permissions ());
307 
308   typedef std::map<Quark, char, AlphabeticalQuarkOrdering> tmp_t;
309   tmp_t tmp;
310   foreach_const (groups_t, _moderated, it) tmp[*it] = 'm';
311   foreach_const (groups_t, _nopost, it) tmp[*it] = 'n';
312 
313   out << "# Permissions: y means posting ok; n means posting not okay; m means moderated.\n"
314       << "# Since almost all groups allow posting, Pan assumes that as the default.\n"
315       << "# Only moderated or no-posting groups are listed here.\n";
316   foreach_const (tmp_t, tmp, it) {
317     out << it->first;
318     out.put (':');
319     out << it->second;
320     out.put ('\n');
321   }
322   data_io.write_done (&out);
323 }
324 
325 /***
326 ****
327 ***/
328 
329 void
ensure_descriptions_are_loaded() const330 DataImpl :: ensure_descriptions_are_loaded () const
331 {
332   if (!_descriptions_loaded)
333   {
334     _descriptions_loaded = true;
335     load_group_descriptions (*_data_io);
336   }
337 }
338 
339 void
load_group_descriptions(const DataIO & data_io) const340 DataImpl :: load_group_descriptions (const DataIO& data_io) const
341 {
342   _descriptions.clear ();
343 
344   LineReader * in (data_io.read_group_descriptions ());
345   StringView s, group;
346   while (in && !in->fail() && in->getline(group))
347     if (group.pop_last_token (s, ':'))
348       _descriptions[group].assign (s.str, s.len);
349   delete in;
350 }
351 
352 void
load_group_xovers(const DataIO & data_io)353 DataImpl :: load_group_xovers (const DataIO& data_io)
354 {
355   LineReader * in (data_io.read_group_xovers ());
356   if (in && !in->fail())
357   {
358     StringView line;
359     StringView groupname, total, unread;
360     StringView xover, servername, low;
361 
362     // walk through the groups line-by-line...
363     while (in->getline (line))
364     {
365       // skip comments and blank lines
366       line.trim();
367       if (line.empty() || *line.str=='#')
368         continue;
369 
370       if (line.pop_token(groupname) && line.pop_token(total) && line.pop_token(unread))
371       {
372         ReadGroup& g (_read_groups[groupname]);
373         g._article_count = strtoul (total.str, NULL, 10);
374         g._unread_count = strtoul (unread.str, NULL, 10);
375 
376         while (line.pop_token (xover))
377           if (xover.pop_token(servername,':'))
378             g[servername]._xover_high = g_ascii_strtoull (xover.str, NULL, 10);
379       }
380     }
381   }
382   delete in;
383 }
384 
385 void
save_group_descriptions(DataIO & data_io) const386 DataImpl :: save_group_descriptions (DataIO& data_io) const
387 {
388   if (_unit_test)
389     return;
390 
391   assert (_descriptions_loaded);
392 
393   typedef std::map<Quark, std::string, AlphabeticalQuarkOrdering> tmp_t;
394   tmp_t tmp;
395   foreach_const (descriptions_t, _descriptions, it)
396     tmp[it->first] = it->second;
397 
398   std::ostream& out (*data_io.write_group_descriptions ());
399   foreach_const (tmp_t, tmp, it) {
400     out << it->first;
401     out.put (':');
402     out << it->second;
403     out.put ('\n');
404   }
405   data_io.write_done (&out);
406 }
407 
408 namespace
409 {
410   typedef std::map < pan::Quark, std::string > quark_to_symbol_t;
411 
412   struct QuarkToSymbol {
413     const quark_to_symbol_t& _map;
~QuarkToSymbol__anon3fde3de10211::QuarkToSymbol414     virtual ~QuarkToSymbol () {}
QuarkToSymbol__anon3fde3de10211::QuarkToSymbol415     QuarkToSymbol (const quark_to_symbol_t& map): _map(map) { }
operator ()__anon3fde3de10211::QuarkToSymbol416     virtual std::string operator() (const Quark& quark) const {
417       quark_to_symbol_t::const_iterator it (_map.find (quark));
418       return it!=_map.end() ? it->second : quark.to_string();
419     }
420   };
421 }
422 
423 void
save_group_xovers(DataIO & data_io) const424 DataImpl :: save_group_xovers (DataIO& data_io) const
425 {
426   if (_unit_test)
427     return;
428 
429   std::ostream& out (*data_io.write_group_xovers ());
430 
431   // find the set of groups that have had an xover
432   typedef std::set<Quark, AlphabeticalQuarkOrdering> xgroups_t;
433   xgroups_t xgroups;
434   foreach_const (read_groups_t, _read_groups, git) {
435     const ReadGroup& group (git->second);
436     bool is_xgroup (group._article_count || group._unread_count);
437     if (!is_xgroup)
438       foreach_const (ReadGroup::servers_t, group._servers, sit)
439         if ((is_xgroup = (sit->second._xover_high!=0)))
440           break;
441     if (is_xgroup)
442       xgroups.insert (git->first);
443   }
444 
445   out << "# groupname totalArticleCount unreadArticleCount [server:latestXoverHigh]*\n";
446 
447   // foreach xgroup
448   foreach_const (xgroups_t, xgroups, it)
449   {
450     const Quark groupname (*it);
451     const ReadGroup& g (*find_read_group (groupname));
452     out << groupname;
453     out.put (' ');
454     out << g._article_count;
455     out.put (' ');
456     out << g._unread_count;
457     foreach_const (ReadGroup::servers_t, g._servers, i) {
458       if (i->second._xover_high) {
459         out.put (' ');
460         out << i->first;
461         out.put (':');
462         out << i->second._xover_high;
463       }
464     }
465     out.put ('\n');
466   }
467 
468   data_io.write_done (&out);
469 }
470 
471 /****
472 *****
473 ****/
474 
475 uint64_t
get_xover_high(const Quark & groupname,const Quark & servername) const476 DataImpl :: get_xover_high (const Quark  & groupname,
477                             const Quark  & servername) const
478 {
479   uint64_t high (0ul);
480   const ReadGroup::Server * rgs (find_read_group_server (groupname, servername));
481   if (rgs)
482     high = rgs->_xover_high;
483   return high;
484 }
485 
486 void
set_xover_high(const Quark & group,const Quark & server,const uint64_t high)487 DataImpl :: set_xover_high (const Quark & group,
488                             const Quark & server,
489                             const uint64_t high)
490 {
491   //std::cerr << LINE_ID << "setting " << get_server_address(server) << ':' << group << " xover high to " << high << std::endl;
492   ReadGroup::Server& rgs (_read_groups[group][server]);
493   rgs._xover_high = high;
494 }
495 
496 void
add_groups(const Quark & server,const NewGroup * newgroups,size_t count)497 DataImpl :: add_groups (const Quark       & server,
498                         const NewGroup    * newgroups,
499                         size_t              count)
500 {
501   ensure_descriptions_are_loaded ();
502 
503   Server * s (find_server (server));
504   assert (s != 0);
505 
506   {
507     AlphabeticalQuarkOrdering o;
508     Server::groups_t groups;
509     std::vector<Quark> tmp;
510 
511     // make a groups_t from the added groups,
512     // and merge it with the server's list of groups
513     groups.get_container().reserve (count);
514     for (const NewGroup *it=newgroups, *end=newgroups+count; it!=end; ++it)
515       groups.get_container().push_back (it->group);
516     groups.sort ();
517     std::set_union (s->groups.begin(), s->groups.end(),
518                     groups.begin(), groups.end(),
519                     std::back_inserter (tmp), o);
520     tmp.erase (std::unique(tmp.begin(), tmp.end()), tmp.end());
521     s->groups.get_container().swap (tmp);
522 
523     // make a groups_t of groups we didn't already have,
524     // and merge it with _unsubscribed (i.e., groups we haven't seen before become unsubscribed)
525     groups.clear ();
526     for (const NewGroup *it=newgroups, *end=newgroups+count; it!=end; ++it)
527       if (!_subscribed.count (it->group))
528         groups.get_container().push_back (it->group);
529     groups.sort ();
530     tmp.clear ();
531     std::set_union (groups.begin(), groups.end(),
532                     _unsubscribed.begin(), _unsubscribed.end(),
533                     std::back_inserter (tmp), o);
534     tmp.erase (std::unique(tmp.begin(), tmp.end()), tmp.end());
535     _unsubscribed.get_container().swap (tmp);
536   }
537 
538   {
539     // build lists of the groups that should and should not be in _moderated and _nopost.t
540     // this is pretty cumbersome, but since these lists almost never change it's still
541     // a worthwhile tradeoff to get the speed/memory wins of a sorted_vector
542     groups_t mod, notmod, post, nopost, tmp;
543     for (const NewGroup *it=newgroups, *end=newgroups+count; it!=end; ++it) {
544       if (it->permission == 'm') mod.get_container().push_back (it->group);
545       if (it->permission != 'm') notmod.get_container().push_back (it->group);
546       if (it->permission == 'n') nopost.get_container().push_back (it->group);
547       if (it->permission != 'n') post.get_container().push_back (it->group);
548     }
549     mod.sort (); notmod.sort ();
550     post.sort (); nopost.sort ();
551 
552     // _moderated -= notmod
553     tmp.clear ();
554     std::set_difference (_moderated.begin(), _moderated.end(), notmod.begin(), notmod.end(), inserter (tmp, tmp.begin()));
555     _moderated.swap (tmp);
556     // _moderated += mod
557     tmp.clear ();
558     std::set_union (_moderated.begin(), _moderated.end(), mod.begin(), mod.end(), inserter (tmp, tmp.begin()));
559     _moderated.swap (tmp);
560 
561     // _nopost -= post
562     tmp.clear ();
563     std::set_difference (_nopost.begin(), _nopost.end(), post.begin(), post.end(), inserter (tmp, tmp.begin()));
564     _nopost.swap (tmp);
565     // _nopost += nopost
566     tmp.clear ();
567     std::set_union (_nopost.begin(), _nopost.end(), nopost.begin(), nopost.end(), inserter (tmp, tmp.begin()));
568     _nopost.swap (tmp);
569   }
570 
571   // keep any descriptions worth keeping that we don't already have...
572   for (const NewGroup *it=newgroups, *end=newgroups+count; it!=end; ++it) {
573     const NewGroup& ng (*it);
574     if (!ng.description.empty() && ng.description!="?")
575       _descriptions[ng.group] = ng.description;
576   }
577 
578   save_group_descriptions (*_data_io);
579   save_group_permissions (*_data_io);
580   fire_grouplist_rebuilt ();
581 }
582 
583 void
mark_group_read(const Quark & groupname)584 DataImpl :: mark_group_read (const Quark& groupname)
585 {
586   ReadGroup * rg (find_read_group (groupname));
587   if (rg != 0) {
588     foreach (ReadGroup::servers_t, rg->_servers, it) {
589       //std::cerr << LINE_ID << " marking read range [0..." << it->second._xover_high << "] in " << get_server_address(it->first) << ']' << std::endl;
590       it->second._read.mark_range (0, it->second._xover_high, true);
591     }
592     rg->_unread_count = 0;
593     save_group_xovers (*_data_io);
594     fire_group_read (groupname);
595   }
596 }
597 
598 void
set_group_subscribed(const Quark & group,bool subscribed)599 DataImpl :: set_group_subscribed (const Quark& group, bool subscribed)
600 {
601   if (subscribed) {
602     _unsubscribed.erase (group);
603     _subscribed.insert (group);
604   } else {
605     _subscribed.erase (group);
606     _unsubscribed.insert (group);
607   }
608 
609   fire_group_subscribe (group, subscribed);
610 }
611 
612 const std::string&
get_group_description(const Quark & group) const613 DataImpl :: get_group_description (const Quark& group) const
614 {
615   ensure_descriptions_are_loaded ();
616   static const std::string nil;
617   descriptions_t::const_iterator it (_descriptions.find (group));
618   return it == _descriptions.end() ? nil : it->second;
619 }
620 
621 void
get_group_counts(const Quark & groupname,unsigned long & unread_count,unsigned long & article_count) const622 DataImpl :: get_group_counts (const Quark   & groupname,
623                               unsigned long & unread_count,
624                               unsigned long & article_count) const
625 {
626   const ReadGroup * g (find_read_group (groupname));
627   if (!g)
628     unread_count = article_count = 0ul;
629   else {
630     unread_count = g->_unread_count;
631     article_count = g->_article_count;
632   }
633 }
634 
635 char
get_group_permission(const Quark & group) const636 DataImpl :: get_group_permission (const Quark & group) const
637 {
638   if (_moderated.count (group))
639     return 'm';
640   else if (_nopost.count (group))
641     return 'n';
642   else
643     return 'y';
644 }
645 
646 
647 void
group_get_servers(const Quark & groupname,quarks_t & addme) const648 DataImpl :: group_get_servers (const Quark& groupname, quarks_t& addme) const
649 {
650   foreach_const (servers_t, _servers, it)
651     if (it->second.groups.count (groupname))
652       addme.insert (it->first);
653 }
654 
655 void
server_get_groups(const Quark & servername,quarks_t & addme) const656 DataImpl :: server_get_groups (const Quark& servername, quarks_t& addme) const
657 {
658   const Server * server (find_server (servername));
659   if (server)
660     addme.insert (server->groups.begin(), server->groups.end());
661 }
662 
663 void
get_subscribed_groups(std::vector<Quark> & setme) const664 DataImpl :: get_subscribed_groups (std::vector<Quark>& setme) const
665 {
666   setme.assign (_subscribed.begin(), _subscribed.end());
667 }
668 
669 void
get_other_groups(std::vector<Quark> & setme) const670 DataImpl :: get_other_groups (std::vector<Quark>& setme) const
671 {
672   setme.assign (_unsubscribed.begin(), _unsubscribed.end());
673 }
674