gui.cxx
Go to the documentation of this file.
1 // ===========================================================================
2 //! \file
3 //! \brief GUI based on FLTK 1.3 / 1.4
4 //!
5 //! Copyright (c) 2012-2026 by the developers. See the LICENSE file for details.
6 //!
7 //! FLTK 1.3 has internal support for Unicode but limited display capabilities
8 //! on some platforms (no glyph substitution with X11 backend by default).
9 //!
10 //! FLTK 1.4 can be compiled with Pango support for the X11 backend. Doing so
11 //! gives extended Unicode display capabilities including glyph substitution
12 //! from multiple fonts.
13 
14 
15 // =============================================================================
16 // Include headers
17 
18 // Include this first for conditional compilation
19 extern "C"
20 {
21 #include "config.h"
22 }
23 
24 // C++98
25 #include <cctype>
26 #include <climits>
27 #include <cmath>
28 #include <cstddef>
29 #include <cstdio>
30 #include <cstdlib>
31 #include <cstring>
32 #include <sstream>
33 
34 // FLTK 1.3
35 #include <FL/Enumerations.H>
36 // Define FL_ABI_VERSION for old versions too
37 #if !defined FL_ABI_VERSION && defined FLTK_ABI_VERSION
38 # define FL_ABI_VERSION FLTK_ABI_VERSION
39 #endif
40 #include <FL/Fl.H>
41 #include <FL/Fl_Box.H>
42 #include <FL/Fl_Button.H>
43 #include <FL/Fl_Check_Button.H>
44 #include <FL/Fl_File_Chooser.H>
45 #include <FL/Fl_Group.H>
46 #include <FL/Fl_Input.H>
47 #include <FL/Fl_Input_Choice.H>
48 #include <FL/Fl_Menu_.H>
49 #include <FL/Fl_Menu_Bar.H>
50 #include <FL/Fl_Menu_Item.H>
51 #include <FL/Fl_Multi_Browser.H>
52 #include <FL/Fl_Pixmap.H>
53 #include <FL/Fl_PostScript.H>
54 #include <FL/Fl_Progress.H>
55 #include <FL/Fl_Printer.H>
56 #include <FL/Fl_Radio_Round_Button.H>
57 #include <FL/Fl_Return_Button.H>
58 #include <FL/Fl_Sys_Menu_Bar.H>
59 #include <FL/Fl_Text_Buffer.H>
60 #include <FL/Fl_Text_Display.H>
61 #include <FL/Fl_Text_Editor.H>
62 #include <FL/Fl_Tabs.H>
63 #include <FL/Fl_Tile.H>
64 #include <FL/Fl_Tree_Item.H>
65 #include <FL/Fl_Tree.H>
66 #include <FL/Fl_Widget.H>
67 #if CFG_DB_DISABLE // Double buffered windows are slow, use only on request
68 # include <FL/Fl_Window.H>
69 #else // CFG_DB_DISABLE
70 # include <FL/Fl_Double_Window.H>
71 #endif // CFG_DB_DISABLE
72 #include <FL/fl_ask.H>
73 
74 extern "C"
75 {
76 // libbasexx
77 #include <libbasexx-0/basexx_version.h>
78 // libssiconv
79 #include <libssiconv-0/iconv.h>
80 // libjpiconv
81 #include <libjpiconv-0/iconv.h>
82 // libuciconv
83 #include <libuciconv-0/iconv.h>
84 }
85 
86 // Local
87 extern "C"
88 {
89 #include "bdate.h"
90 #include "conf.h"
91 #include "core.h"
92 #include "encoding.h"
93 #include "extutils.h"
94 #include "filter.h"
95 #include "log.h"
96 #include "nls.h"
97 #include "xdg.h"
98 }
99 #include "main.hxx"
100 #include "tls.hxx"
101 #include "ui.hxx"
102 
103 
104 // =============================================================================
105 //! \defgroup GUI GUI: Graphical User Interface
106 //!
107 //! This graphical user interface is based on FLTK 1.3 and is intended to work
108 //! with a FLTK 1.3.0 shared library. Using features that are added later is not
109 //! allowed (without ABI guards) to preserve backward compatibility.
110 //!
111 //! \note
112 //! If you hear bugging beeps when information windows appear, this file is the
113 //! wrong place to fix. These beeps are created by FLTK internally and can be
114 //! eliminated by patching the file 'fltk-1.3.0/src/fl_ask.cxx'. Commenting out
115 //! all calls to 'fl_beep()' and rebuilding FLTK will give you comfortable
116 //! silence. Newer FLTK versions already contains this "silence patch".
117 //! @{
118 
119 
120 // =============================================================================
121 // Experimental features
122 
123 //! Setting this to 1 enables line count in article tree/list (EXPERIMENTAL)
124 #define USE_LINE_COUNT 0
125 
126 //! Setting this to 1 enables article number in article tree/list (EXPERIMENTAL)
127 #define USE_ARTICLE_NUMBER 0
128 
129 
130 // =============================================================================
131 // Constants
132 
133 //! \brief Message prefix for MAIN module
134 #define MAIN_ERR_PREFIX "GUI: "
135 
136 //! \name Fixed widget colors
137 //!
138 //! Override some FLTK defaults that can't be configured via X resources.
139 //! @{
140 //! Selection color for menu
141 #define UI_COLOR_MENU_SELECTION (Fl_Color) 0x50505000UL
142 //! Color for progress bar
143 #define UI_COLOR_PROGRESS_BAR (Fl_Color) 0x50505000UL
144 //! Color for selected radio button
145 #define UI_COLOR_RADIO_BUTTON (Fl_Color) 0x50505000UL
146 //! @}
147 
148 //! Number of styles for article content syntax highlighting
149 #define UI_STYLES_LEN 9
150 
151 //! \name Callback action controls
152 //! @{
153 #define UI_CB_START 0
154 #define UI_CB_CONTINUE 1
155 #define UI_CB_FINISH 2
156 //! @}
157 
158 //! \name Callback cookies
159 //! @{
160 #define UI_CB_COOKIE_SERVER 0U
161 #define UI_CB_COOKIE_GROUPLIST 1U
162 #define UI_CB_COOKIE_GROUPLABELS 2U
163 #define UI_CB_COOKIE_GROUPPROPOSAL 3U
164 #define UI_CB_COOKIE_GROUPINFO1 4U
165 #define UI_CB_COOKIE_GROUPINFO2 5U
166 #define UI_CB_COOKIE_GROUP 6U
167 #define UI_CB_COOKIE_OVERVIEW 7U
168 #define UI_CB_COOKIE_HEADER 8U
169 #define UI_CB_COOKIE_BODY 9U
170 #define UI_CB_COOKIE_MOTD 10U
171 #define UI_CB_COOKIE_ARTICLE 11U
172 #define UI_CB_COOKIE_SRC 12U
173 #define UI_CB_COOKIE_POST 13U
174 //! @}
175 
176 //! \name Encryption algorithms
177 //!
178 //! All nonzero values additionally provide server to client authentication via
179 //! X509 certificate.
180 //! @{
181 #define UI_ENC_NONE 0 // No encryption
182 #define UI_ENC_STRONG 1 // TLS with strong encryption and forward secrecy
183 #define UI_ENC_WEAK 2 // TLS compatibility mode offering weak cipher suites
184 //! @}
185 
186 //! \name Authentication algorithms (client to server)
187 //! @{
188 #define UI_AUTH_NONE 0 // No authentication
189 #define UI_AUTH_USER 1 // AUTHINFO USER/PASS as defined in RFC 4643
190 //! @}
191 
192 //! \name Limits for clamp article count (CONF_CAC) configuration value
193 //! @{
194 #define UI_CAC_MIN 10 // Lower limit: 1
195 #define UI_CAC_MAX 50000 // Upper limit: INT_MAX
196 //! @}
197 
198 //! Size of buffer for header field creation
199 #define UI_HDR_BUFSIZE (std::size_t) 998
200 
201 //! Static buffer size for compose window style update callback
202 #define UI_STATIC_STYLE_BUFSIZE (std::size_t) 80
203 
204 //! Maximum number of groups for crossposting
205 #define UI_XPOST_LIMIT (std::size_t) 10
206 
207 //! Enable references line in ArticleWindow class
208 //!
209 //! This is currently not useful, because ArticleWindow has no hyperlink
210 //! support.
211 #define UI_AW_REFERENCES 0
212 
213 // Use window class with or without double buffering
214 #if CFG_DB_DISABLE
215 # define UI_WINDOW_CLASS Fl_Window
216 #else // CFG_DB_DISABLE
217 # define UI_WINDOW_CLASS Fl_Double_Window
218 #endif // CFG_DB_DISABLE
219 
220 // Enable window icons on X11 if possible
221 #define USE_WINDOW_ICON 0
222 #ifdef FL_ABI_VERSION
223 # if 10303 <= FL_ABI_VERSION
224  // At least FLTK 1.3.3 is required
225 # undef USE_WINDOW_ICON
226 # define USE_WINDOW_ICON 1
227 # endif // 10303 <= FL_ABI_VERSION
228 #endif // FL_ABI_VERSION
229 
230 
231 // =============================================================================
232 // Data types
233 
234 // Target position for scrolling
235 enum ui_scroll
236 {
237  UI_SCROLL_NONE,
238  UI_SCROLL_TOP,
239  UI_SCROLL_MIDDLE,
240  UI_SCROLL_BOTTOM
241 };
242 
243 // Server configuration
244 class ServerConfig
245 {
246  public:
247  char server[128];
248  char service[6];
249  int enc; // Use UI_ENC_xxx constants
250  int auth; // Use UI_AUTH_xxx constants
251 
252  inline void serverReplace(const char* s)
253  {
254  std::strncpy(server, s, 128); server[127] = 0;
255  }
256  inline void serviceReplace(const char* s)
257  {
258  std::strncpy(service, s, 6); service[5] = 0;
259  }
260  inline void serviceReplace(unsigned int i)
261  {
262  std::ostringstream ss;
263 
264  if(0xFFFFU < i) { i = 0xFFFFU; }
265  ss << i << std::flush;
266  const std::string& s = ss.str();
267  std::strncpy(service, s.c_str(), 6); service[5] = 0;
268  }
269 };
270 
271 // MIME multipart content
272 class MIMEContent
273 {
274  struct MIMEContentListElement
275  {
276  enc_mime_cte cte;
277  enc_mime_ct ct;
278  const char* header;
279  const char* content;
280  enum enc_mime_cd type; // Content-Disposition
281  const char* filename; // From Content-Disposition "filename" parameter
282  MIMEContentListElement* next;
283  };
284 
285  private:
286  bool multipart; // Multipart flag (to enable display of entity headers)
287  std::size_t partNum; // Number of elements in linked list
288  MIMEContentListElement* partList; // Linked list
289 
290  const char* createMessageHeader(const char*, std::size_t);
291  MIMEContentListElement* decodeElement(const char*, std::size_t, char*,
292  bool, bool, std::size_t*);
293  MIMEContentListElement* initElement(const char*, std::size_t,
295  struct core_article_header*);
296  public:
297  inline bool is_multipart(void) { return(multipart); }
298  inline std::size_t parts(void) { return(partNum); }
299  inline const char* part_header(std::size_t i)
300  {
301  MIMEContentListElement* mcle = partList;
302 
303  if(!partNum || i >= partNum) { return(NULL); }
304  else
305  {
306  while(i--) { mcle = mcle->next; }
307  return(mcle->header);
308  }
309  }
310  inline const char* part(std::size_t i, enc_mime_cte* te,
311  enc_mime_ct** ctp)
312  {
313  MIMEContentListElement* mcle = partList;
314 
315  if(!partNum || i >= partNum) { return(NULL); }
316  else
317  {
318  while(i--) { mcle = mcle->next; }
319  *te = mcle->cte;
320  if(NULL != ctp) { *ctp = &mcle->ct; }
321  return(mcle->content);
322  }
323  }
324  inline enum enc_mime_cd type(std::size_t i)
325  {
326  MIMEContentListElement* mcle = partList;
327 
328  if(!partNum || i >= partNum) { return(ENC_CD_UNKNOWN); }
329  else
330  {
331  while(i--) { mcle = mcle->next; }
332  return(mcle->type);
333  }
334  }
335  inline const char* filename(std::size_t i)
336  {
337  MIMEContentListElement* mcle = partList;
338 
339  if(!partNum || i >= partNum) { return(NULL); }
340  else
341  {
342  while(i--) { mcle = mcle->next; }
343  return(mcle->filename);
344  }
345  }
346  MIMEContent(struct core_article_header*, const char*);
347  ~MIMEContent(void);
348 };
349 
350 // A clone of Fl_Multi_Browser
351 // Should work like Fl_Hold_Browser, but with focus navigation via keyboard
352 class My_Multi_Browser : public Fl_Multi_Browser
353 {
354  private:
355  int handle(int);
356  public:
357  My_Multi_Browser(int X, int Y, int W, int H, const char* L = 0)
358  : Fl_Multi_Browser(X, Y, W, H, L) { }
359 };
360 
361 // A clone of Fl_Text_Display
362 class My_Text_Display : public Fl_Text_Display
363 {
364  private:
365  int linkPushed;
366 
367  int handle(int);
368  public:
369  My_Text_Display(int X, int Y, int W, int H, const char* L = 0)
370  : Fl_Text_Display(X, Y, W, H, L) { linkPushed = -1; }
371 };
372 
373 // A clone of Fl_Tree
374 class My_Tree : public Fl_Tree
375 {
376  private:
377  bool updated; // Used to block article selection while updating
378  bool positions_recalculated;
379  Fl_Tree_Item* current_article;
380 
381  int handle(int);
382  public:
383  inline void update_in_progress(bool state) { updated = !state; }
384  inline bool update_in_progress(void) { return !updated; }
385  inline void not_drawn(void) { positions_recalculated = false; }
386  inline bool drawn(void)
387  {
388  // Workaround for "infinite busy" without drawing area
389  if (0 == w() || 0 == h()) { return true; }
390  return(positions_recalculated);
391  }
392  inline void draw(void)
393  {
394  // This will calculate the positions of the tree items
395  Fl_Tree::draw();
396  positions_recalculated = true;
397  }
398  inline void store_current(Fl_Tree_Item* ti)
399  {
400  current_article = ti;
401  }
402  inline void select_former(void)
403  {
404  if(NULL != current_article)
405  {
406  set_item_focus(current_article);
407  select_only(current_article, 0);
408  }
409  }
410  My_Tree(int X, int Y, int W, int H, const char* L = 0)
411  : Fl_Tree(X, Y, W, H, L)
412  {
413  updated = true;
414  positions_recalculated = false;
415  current_article = NULL;
416  }
417 };
418 
419 // Server configuration window
420 class ServerCfgWindow : public UI_WINDOW_CLASS
421 {
422  private:
423  ServerConfig* sconf;
424  int finished;
425  Fl_Group* grpAuth;
426  Fl_Group* scfgGroup;
427  Fl_Input* scfgHostname;
428  Fl_Input* scfgService;
429  Fl_Radio_Round_Button* scfgTlsOff;
430  Fl_Radio_Round_Button* scfgTlsStrong;
431  Fl_Radio_Round_Button* scfgTlsWeak;
432  Fl_Radio_Round_Button* scfgAuthOff;
433  Fl_Radio_Round_Button* scfgAuthUser;
434  char* hostname;
435  char* service;
436  char* tls_headline;
437 
438  // OK callback
439  inline void ok_cb_i(void) { finished = 1; }
440  static void ok_cb(Fl_Widget*, void* w)
441  {
442  ((ServerCfgWindow*) w)->ok_cb_i();
443  }
444 
445  // Cancel callback
446  inline void cancel_cb_i(void) { finished = -1; }
447  static void cancel_cb(Fl_Widget*, void* w)
448  {
449  ((ServerCfgWindow*) w)->cancel_cb_i();
450  }
451 
452  // Encryption radio buttons callback
453  inline void enc_cb_i(bool enc)
454  {
455  if(!enc)
456  {
457  scfgService->value("nntp");
458 #if !CFG_NNTP_AUTH_UNENCRYPTED
459  scfgAuthOff->setonly();
460  grpAuth->deactivate();
461 #endif
462  }
463  else
464  {
465  scfgService->value("nntps");
466  grpAuth->activate();
467  }
468  }
469  static void enc_off_cb(Fl_Widget*, void* w)
470  {
471  ((ServerCfgWindow*) w)->enc_cb_i(false);
472  }
473  static void enc_on_cb(Fl_Widget*, void* w)
474  {
475  ((ServerCfgWindow*) w)->enc_cb_i(true);
476  }
477 
478  public:
479  int process(void);
480  ServerCfgWindow(ServerConfig*, const char*);
481  ~ServerCfgWindow(void);
482 };
483 
484 // Identity configuration window
485 class IdentityCfgWindow : public UI_WINDOW_CLASS
486 {
487  private:
488  Fl_Group* cfgGroup;
489  Fl_Input* fromName;
490  Fl_Input* fromEmail;
491  Fl_Input* replytoName;
492  Fl_Input* replytoEmail;
493 
494  // OK callback
495  inline void ok_cb_i(void);
496  static void ok_cb(Fl_Widget*, void* w)
497  {
498  ((IdentityCfgWindow*) w)->ok_cb_i();
499  }
500 
501  // Cancel callback
502  static void cancel_cb(Fl_Widget*, void* w)
503  {
504  Fl::delete_widget((Fl_Widget*) w);
505  }
506  public:
507  IdentityCfgWindow(const char*);
508  ~IdentityCfgWindow(void);
509 };
510 
511 // Misc configuration window
512 class MiscCfgWindow : public UI_WINDOW_CLASS
513 {
514  private:
515  Fl_Tabs* cfgTabs;
516  Fl_Group* cacGroup;
517  Fl_Input* cacField;
518  Fl_Check_Button* cmprEnable;
519  Fl_Check_Button* localTime;
520  Fl_Check_Button* uagentEnable;
521  Fl_Group* qsGroup;
522  Fl_Check_Button* qsSpace;
523  Fl_Check_Button* qsUnify;
524 
525  // OK callback
526  inline void ok_cb_i(void);
527  static void ok_cb(Fl_Widget*, void* w)
528  {
529  ((MiscCfgWindow*) w)->ok_cb_i();
530  }
531 
532  // Cancel callback
533  static void cancel_cb(Fl_Widget*, void* w)
534  {
535  Fl::delete_widget((Fl_Widget*) w);
536  }
537  public:
538  MiscCfgWindow(const char*);
539  ~MiscCfgWindow(void);
540 };
541 
542 
543 // Search window
544 class SearchWindow : public UI_WINDOW_CLASS
545 {
546  private:
547  Fl_Input* searchField;
548  Fl_Check_Button* cisEnable;
549  const char** currentSearchString;
550 
551  // OK callback
552  inline void ok_cb_i(void);
553  static void ok_cb(Fl_Widget*, void* w)
554  {
555  ((SearchWindow*) w)->ok_cb_i();
556  ((SearchWindow*) w)->finished = 1;
557  }
558 
559  // Cancel callback
560  static void cancel_cb(Fl_Widget*, void* w)
561  {
562  ((SearchWindow*) w)->finished = -1;
563  }
564  public:
565  int finished;
566  SearchWindow(const char*, const char**);
567  ~SearchWindow(void);
568 };
569 
570 
571 // Protocol console window
572 class ProtocolConsole : public UI_WINDOW_CLASS
573 {
574  public:
575  Fl_Text_Display* consoleDisplay;
576  private:
577  Fl_Text_Buffer* consoleText;
578  FILE* logfp;
579  int nolog;
580 
581  // Exit callback
582  static void exit_cb(Fl_Widget*, void* w)
583  {
584  Fl::delete_widget((Fl_Widget*) w);
585  }
586 
587  public:
588  void update(void);
589  ProtocolConsole(const char*);
590  ~ProtocolConsole(void);
591 };
592 
593 // Subscribe window
594 class SubscribeWindow : public UI_WINDOW_CLASS
595 {
596  private:
597  Fl_Group* subscribeGroup;
598  Fl_Tree* subscribeTree;
599  core_groupdesc* grouplist;
600  core_grouplabel* grouplabels;
601 
602  // OK callback
603  inline void ok_cb_i(void);
604  static void ok_cb(Fl_Widget*, void* w)
605  {
606  ((SubscribeWindow*) w)->ok_cb_i();
607  }
608 
609  // Cancel callback
610  static void cancel_cb(Fl_Widget*, void* w)
611  {
612  Fl::delete_widget((Fl_Widget*) w);
613  }
614 
615  // Tree callback
616  inline void tree_cb_i(void)
617  {
618  Fl_Tree_Item* ti;
619 
620  if(FL_TREE_REASON_SELECTED == subscribeTree->callback_reason())
621  {
622  // Immediately reset selection again if item is not selectable
623  ti = subscribeTree->callback_item();
624  if(NULL == ti->user_data()) { subscribeTree->deselect(ti, 0); }
625  }
626  }
627  static void tree_cb(Fl_Widget*, void* w)
628  {
629  ((SubscribeWindow*) w)->tree_cb_i();
630  }
631  public:
632  inline void add(const char* entry, core_anum_t num, const char* label)
633  {
634  Fl_Tree_Item* ti;
635  std::ostringstream labelString;
636 
637  ti = subscribeTree->find_item(entry);
638  if(NULL == ti) { ti = subscribeTree->add(entry); }
639  if(NULL == ti)
640  {
641  PRINT_ERROR("Adding group to subscription tree failed");
642  }
643  else
644  {
645  // Set all selectable items to bold font and tag them with user data
646  ti->labelfont(FL_HELVETICA_BOLD);
647  ti->user_data((void*) entry);
648  labelString << ti->label() << " (" << num << ")";
649  if(NULL != label) { labelString << " | " << label; }
650  // Attention:
651  //
652  // const char* ls = labelString.str().c_str()
653  //
654  // creates 'ls' with undefined value because the compiler is
655  // allowed to free the temporary string object returned by
656  // 'str()' immediately after the assignment!
657  // Assigning names to temporary string objects forces them to
658  // stay in memory as long as their names go out of scope (this
659  // is what we need).
660  const std::string& ls = labelString.str();
661  ti->label(ls.c_str());
662  }
663  }
664  inline void collapseAll(void)
665  {
666  Fl_Tree_Item* i;
667 
668  for(i = subscribeTree->first(); i; i = subscribeTree->next(i))
669  {
670  subscribeTree->close(i, 0);
671  }
672  subscribeTree->open(subscribeTree->root(), 0);
673  }
674  SubscribeWindow(const char* label, core_groupdesc* glist,
675  core_grouplabel* labels);
676  ~SubscribeWindow(void);
677 };
678 
679 // MID search window
680 class MIDSearchWindow : public UI_WINDOW_CLASS
681 {
682  private:
683  Fl_Group* cfgGroup;
684  Fl_Input* mid;
685 
686  // OK callback
687  inline void ok_cb_i(void);
688  static void ok_cb(Fl_Widget*, void* w)
689  {
690  ((MIDSearchWindow*) w)->ok_cb_i();
691  }
692 
693  // Cancel callback
694  static void cancel_cb(Fl_Widget*, void* w)
695  {
696  Fl::delete_widget((Fl_Widget*) w);
697  }
698  public:
699  MIDSearchWindow(const char*);
700  ~MIDSearchWindow(void);
701 };
702 
703 // Bug report window
704 class BugreportWindow : public UI_WINDOW_CLASS
705 {
706  private:
707  Fl_Text_Display* bugreportDisplay;
708  Fl_Text_Buffer* bugreportText;
709 
710  // Exit callback
711  static void exit_cb(Fl_Widget*, void* w)
712  {
713  Fl::delete_widget((Fl_Widget*) w);
714  }
715 
716  public:
717  BugreportWindow(const char*, const char*);
718  ~BugreportWindow(void);
719 };
720 
721 // License window
722 class LicenseWindow : public UI_WINDOW_CLASS
723 {
724  private:
725  Fl_Text_Display* licenseDisplay;
726  Fl_Text_Buffer* licenseText;
727 
728  // Exit callback
729  static void exit_cb(Fl_Widget*, void* w)
730  {
731  Fl::delete_widget((Fl_Widget*) w);
732  }
733 
734  public:
735  LicenseWindow(const char*);
736  ~LicenseWindow(void);
737 };
738 
739 // Message of the day window
740 class MotdWindow : public UI_WINDOW_CLASS
741 {
742  private:
743  Fl_Text_Display* motdDisplay;
744  Fl_Text_Buffer* motdText;
745 
746  // Exit callback
747  static void exit_cb(Fl_Widget*, void* w)
748  {
749  Fl::delete_widget((Fl_Widget*) w);
750  }
751 
752  public:
753  MotdWindow(const char*, const char*);
754  ~MotdWindow(void);
755 };
756 
757 // Article window
758 class ArticleWindow : public UI_WINDOW_CLASS
759 {
760  private:
761  Fl_Group* articleGroup;
762  My_Text_Display* articleDisplay;
763  Fl_Text_Buffer* articleText;
764  Fl_Text_Buffer* articleStyle;
765  Fl_Text_Display::Style_Table_Entry* styles;
766  int styles_len;
767  MIMEContent* mimeData;
768  struct core_hierarchy_element* alt_hier;
769 
770  const char* printHeaderFields(struct core_article_header*);
771  void articleUpdate(Fl_Text_Buffer* article);
772 
773  // Exit callback
774  static void cancel_cb(Fl_Widget*, void* w)
775  {
776  Fl::delete_widget((Fl_Widget*) w);
777  }
778 
779  public:
780  ArticleWindow(const char*, const char*);
781  ~ArticleWindow(void);
782 };
783 
784 // Article source window
785 class ArticleSrcWindow : public UI_WINDOW_CLASS
786 {
787  private:
788  Fl_Group* srcGroup;
789  Fl_Text_Display* srcDisplay;
790  Fl_Text_Buffer* srcText;
791  Fl_Text_Buffer* srcStyle;
792  Fl_Text_Display::Style_Table_Entry* styles;
793  char* srcArticle;
794  const char* pathname;
795 
796  // Save callback
797  inline void save_cb_i(void);
798  static void save_cb(Fl_Widget*, void* w)
799  {
800  ((ArticleSrcWindow*) w)->save_cb_i();
801  }
802 
803  // Exit callback
804  static void cancel_cb(Fl_Widget*, void* w)
805  {
806  Fl::delete_widget((Fl_Widget*) w);
807  }
808 
809  public:
810  ArticleSrcWindow(const char*, const char*);
811  ~ArticleSrcWindow(void);
812 };
813 
814 // Compose window
815 class ComposeWindow : public UI_WINDOW_CLASS
816 {
817  private:
818  Fl_Tabs* compTabs;
819  Fl_Group* subjectGroup;
820  Fl_Input* subjectField;
821  Fl_Group* compGroup;
822  Fl_Group* uriEncGroup;
823  Fl_Group* advancedGroup;
824  Fl_Text_Buffer* compHeader;
825  Fl_Text_Display::Style_Table_Entry* styles;
826  Fl_Text_Buffer* currentStyle;
827  Fl_Text_Buffer* compText;
828  // On URI decoder tab
829  Fl_Box* uriHeaderField;
830  Fl_Input_Choice* uriSchemeField;
831  Fl_Input* uriBodyField;
832  // On advanced tab
833  Fl_Input* newsgroupsField;
834  Fl_Input_Choice* fup2Field;
835  Fl_Input_Choice* keywordField;
836  Fl_Input* expireField;
837  Fl_Input_Choice* distriField;
838  Fl_Check_Button* archiveButton;
839  Fl_Box* fillSpace;
840 
841  // Change subject callback
842  inline void change_cb_i(void);
843  static void change_cb(Fl_Widget*, void* w)
844  {
845  ((ComposeWindow*) w)->change_cb_i();
846  }
847 
848  // Style update callback
849  inline void style_update_cb_i(int pos, int nInserted, int nDeleted,
850  Fl_Text_Buffer* style,
851  Fl_Text_Editor* editor);
852  static void style_update_cb(int pos, int nInserted, int nDeleted,
853  int nRestyled, const char* deletedText,
854  void* w)
855  {
856  (void) nRestyled;
857  (void) deletedText;
858  ((ComposeWindow*) w)->style_update_cb_i(pos, nInserted, nDeleted,
859  ((ComposeWindow*) w)->currentStyle,
860  ((ComposeWindow*) w)->compEditor);
861  }
862 
863  // Send callback
864  inline void send_cb_i(void);
865  static void send_cb(Fl_Widget*, void* w)
866  {
867  ((ComposeWindow*) w)->send_cb_i();
868  }
869 
870  // Cancel callback
871  inline void cancel_cb_i(Fl_Widget*);
872  static void cancel_cb(Fl_Widget*, void* w)
873  {
874  ((ComposeWindow*) w)->cancel_cb_i((Fl_Widget*) w);
875  }
876 
877  // URI insert callback
878  inline void uri_insert_cb_i(void);
879  static void uri_insert_cb(Fl_Widget*, void* w)
880  {
881  ((ComposeWindow*) w)->uri_insert_cb_i();
882  }
883 
884  int searchHeaderField(const char*, int*, int*);
885  const char* extractHeaderField(const char*);
886  int replaceHeaderField(const char*, const char*);
887  void deleteHeaderField(const char*);
888  int checkArticleBody(const char*);
889  public:
890  Fl_Text_Editor* compEditor;
891  ComposeWindow(const char*, const char*, const char*, const char*,
892  struct core_article_header*, bool);
893  ~ComposeWindow(void);
894 };
895 
896 // Main window
897 class MainWindow : public UI_WINDOW_CLASS
898 {
899  // States for the callback locking state machine
900  enum mainWindowState
901  {
902  STATE_READY,
903  STATE_MUTEX, // Allow nothing else to run in parallel
904  STATE_SERVER1,
905  STATE_SERVER2,
906  STATE_PROPOSAL,
907  STATE_GROUP,
908  STATE_SCROLL,
909  STATE_NEXT,
910  STATE_COMPOSE,
911  STATE_POST
912  };
913 
914  // Events for the callback locking state machine
915  enum mainWindowEvent
916  {
917  // Server configuration
918  EVENT_SERVER,
919  EVENT_SERVER_EXIT,
920  // Group subscription
921  EVENT_SUBSCRIBE,
922  EVENT_SUBSCRIBE_EXIT,
923  // Group list proposals
924  EVENT_GL_PROPOSAL,
925  EVENT_GL_PROPOSAL_EXIT,
926  // Group list refresh
927  EVENT_GL_REFRESH,
928  EVENT_GL_REFRESH_EXIT,
929  // Group selection
930  EVENT_G_SELECT,
931  EVENT_G_SELECT_EXIT,
932  // Article tree refresh
933  EVENT_AT_REFRESH,
934  EVENT_AT_REFRESH_EXIT,
935  // Prepare for article selection
936  EVENT_A_PREPARE,
937  // Article selection
938  EVENT_A_SELECT,
939  EVENT_A_SELECT_EXIT,
940  // View article (not from current group)
941  EVENT_A_VIEW,
942  EVENT_A_VIEW_EXIT,
943  // Mark articles read (current group)
944  EVENT_A_MAAR,
945  EVENT_A_MAAR_EXIT,
946  // Mark articles read (all groups)
947  EVENT_A_MAGAR,
948  EVENT_A_MAGAR_EXIT,
949  // View source code
950  EVENT_SRC_VIEW,
951  EVENT_SRC_VIEW_EXIT,
952  // View message of the day
953  EVENT_MOTD_VIEW,
954  EVENT_MOTD_VIEW_EXIT,
955  // Scroll down or (at the end) select next unread article
956  EVENT_SCROLL_NEXT,
957  EVENT_SCROLL_NEXT_EXIT,
958  // Article composition
959  EVENT_COMPOSE,
960  EVENT_COMPOSE_EXIT,
961  // Article posting
962  EVENT_POST,
963  EVENT_POST_EXIT
964  };
965 
966  public:
967  Fl_Box* statusBar;
968  Fl_Progress* progressBar;
969  std::ostringstream aboutString;
970  ComposeWindow* composeWindow;
971  int composeWindowLock;
972  unsigned int hyperlinkStyle;
973  int hyperlinkPosition;
974  private:
975  bool startup; // Flag to preserve greeting message on startup
976  bool busy; // Mouse cursor state
977  bool unsub; // Flag to indicate current group was unsubscribed
978  mainWindowState mainState;
979  SubscribeWindow* subscribeWindow;
980  Fl_Tile* contentGroup;
981  Fl_Tile* contentGroup2;
982 #if CFG_COCOA_SYS_MENUBAR
983  Fl_Sys_Menu_Bar* menu;
984 #else // CFG_COCOA_SYS_MENUBAR
985  Fl_Menu_Bar* menu;
986 #endif // CFG_COCOA_SYS_MENUBAR
987  std::size_t groupcount;
988  core_groupdesc* grouplist;
989  My_Multi_Browser* groupList;
990  core_groupdesc* subscribedGroups; // Array of subscribed groups
991  core_groupdesc* currentGroup; // Descriptor of current group
992  My_Tree* articleTree;
993  core_hierarchy_element* currentArticleHE;
994  core_hierarchy_element* lastArticleHE;
995  My_Text_Display* text;
996  int wrapMode;
997  Fl_Text_Display::Style_Table_Entry* styles;
998  int styles_len;
999  Fl_Text_Buffer* currentStyle;
1000  Fl_Text_Buffer* currentArticle;
1001  int currentLine;
1002  const char* currentSearchString;
1003  int currentSearchPosition;
1004  int state; // -1: Abort, 0: Ready, 1: Finished, 2: Empty
1005  core_anum_t ai; // Article ID (watermark)
1006  core_range ai_range; // Article ID (watermark) range
1007  int groupSelect_cb_state;
1008  int groupRefresh_cb_state;
1009  float progress_percent_value;
1010  char progress_percent_label[5];
1011  bool progress_skip_update;
1012  MIMEContent* mimeData;
1013  char* mid_a;
1014  bool rot13; // ROT13 applied. Recovery requires reload of article
1015 
1016  // The group states are loaded/stored from/to the groupfile
1017  // Note:
1018  // The 'name' fields in the group descriptor array 'subscribedGroups'
1019  // point to the memory of the corresponding 'name' fields of 'group_list'.
1020  core_groupstate* group_list; // Array of group states
1021  std::size_t group_num; // Number of elements in group state array
1022  std::size_t group_list_index; // Index of current group in 'group_list'
1023  int group_old; // Previous selected value in 'groupList' widget
1024  int group_new; // Selected value in 'groupList' widget
1025 
1026  // Exit callback
1027  inline void progress_release_cb_i(void)
1028  {
1029  progress_skip_update = false;
1030  }
1031  static void progress_release_cb(void* w)
1032  {
1033  ((MainWindow*) w)->progress_release_cb_i();
1034  }
1035 
1036  // Exit callback
1037  inline void exit_cb_i(void);
1038  static void exit_cb(Fl_Widget*, void* w)
1039  {
1040  ((MainWindow*) w)->exit_cb_i();
1041  }
1042 
1043  // Print cooked article callback
1044  inline void print_cb_i(void);
1045  static void print_cb(Fl_Widget*, void* w)
1046  {
1047 #if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1048  // Disable GTK print dialog that offer features that do not work
1049  Fl::option(Fl::OPTION_PRINTER_USES_GTK, false);
1050 #endif // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1051  ((MainWindow*) w)->print_cb_i();
1052  }
1053 
1054  // Save cooked article (to file) callback
1055  inline void asave_cb_i(void);
1056  static void asave_cb(Fl_Widget*, void* w)
1057  {
1058  ((MainWindow*) w)->asave_cb_i();
1059  }
1060 
1061  // Server callback
1062  inline void server_cb_i(void)
1063  {
1064  updateServer(UI_CB_START);
1065  }
1066  static void server_cb(Fl_Widget*, void* w)
1067  {
1068  ((MainWindow*) w)->server_cb_i();
1069  }
1070 
1071  // Configuration callback
1072  inline void config_cb_i(void);
1073  static void config_cb(Fl_Widget*, void* w)
1074  {
1075  ((MainWindow*) w)->config_cb_i();
1076  }
1077 
1078  // Identity callback
1079  inline void identity_cb_i(void);
1080  static void identity_cb(Fl_Widget*, void* w)
1081  {
1082  ((MainWindow*) w)->identity_cb_i();
1083  }
1084 
1085  // About information callback
1086  inline void about_cb_i(void);
1087  static void about_cb(Fl_Widget*, void* w)
1088  {
1089  ((MainWindow*) w)->about_cb_i();
1090  }
1091 
1092  // View message of the day callback
1093  inline void viewmotd_cb_i(void)
1094  {
1095  viewMotd(UI_CB_START);
1096  }
1097  static void viewmotd_cb(Fl_Widget*, void* w)
1098  {
1099  ((MainWindow*) w)->viewmotd_cb_i();
1100  }
1101 
1102  // MID search callback
1103  inline void mid_search_cb_i(void);
1104  static void mid_search_cb(Fl_Widget*, void* w)
1105  {
1106  ((MainWindow*) w)->mid_search_cb_i();
1107  }
1108 
1109  // Bug callback
1110  inline void bug_cb_i(void);
1111  static void bug_cb(Fl_Widget*, void* w)
1112  {
1113  ((MainWindow*) w)->bug_cb_i();
1114  }
1115 
1116  // License information callback
1117  inline void license_cb_i(void);
1118  static void license_cb(Fl_Widget*, void* w)
1119  {
1120  ((MainWindow*) w)->license_cb_i();
1121  }
1122 
1123  // Protocol console callback
1124  inline void console_cb_i(void);
1125  static void console_cb(Fl_Widget*, void* w)
1126  {
1127  ((MainWindow*) w)->console_cb_i();
1128  }
1129 
1130  // Subscribe callback
1131  inline void gsubscribe_cb_i(void)
1132  {
1133  groupSubscribe(UI_CB_START);
1134  }
1135  static void gsubscribe_cb(Fl_Widget*, void* w)
1136  {
1137  ((MainWindow*) w)->gsubscribe_cb_i();
1138  }
1139 
1140  // Unsubscribe callback
1141  inline void gunsubscribe_cb_i(void)
1142  {
1143  std::size_t index = (std::size_t) groupList->value();
1144  int rv;
1145 
1146  // Ask for confirmation
1147  fl_message_title(S("Warning"));
1148  rv = fl_choice("%s", S("No"),
1149  S("Yes"), NULL,
1150  S("Really unsubscribe group?"));
1151  if(!rv) { return; }
1152 
1153  if(!index)
1154  {
1155  SC("Do not use non-ASCII for the translation of this item")
1156  fl_message_title(S("Error"));
1157  fl_alert("%s", S("No group selected"));
1158  }
1159  else
1160  {
1161  rv = core_unsubscribe_group(&group_num, &group_list,
1162  &group_list_index);
1163  if(rv)
1164  {
1165  fl_message_title(S("Error"));
1166  fl_alert("%s", S("Unsubscribe operation failed"));
1167  }
1168  rv = core_export_group_states(group_num, group_list);
1169  if(rv)
1170  {
1171  fl_message_title(S("Error"));
1172  fl_alert("%s", S("Exporting group states failed"));
1173  }
1174  unsub = true;
1175  groupListRefresh(UI_CB_START);
1176  }
1177  }
1178  static void gunsubscribe_cb(Fl_Widget*, void* w)
1179  {
1180  ((MainWindow*) w)->gunsubscribe_cb_i();
1181  }
1182 
1183  // Group list refresh callback
1184  inline void grefresh_cb_i(void)
1185  {
1186  groupListRefresh(UI_CB_START);
1187  }
1188  static void grefresh_cb(Fl_Widget*, void* w)
1189  {
1190  ((MainWindow*) w)->grefresh_cb_i();
1191  }
1192 
1193  // Sort group list callback
1194  inline void gsort_cb_i(void)
1195  {
1196  int rv;
1197 
1198  // Ask for confirmation
1199  fl_message_title(S("Warning"));
1200  rv = fl_choice("%s", S("No"),
1201  S("Yes"), NULL,
1202  S("Really sort group list?"));
1203  if(!rv) { return; }
1204 
1205  rv = core_sort_group_list();
1206  if(!rv)
1207  {
1208  // Set unsubscribe flag to clear tree and current group/article
1209  unsub = true;
1210  groupListRefresh(UI_CB_START);
1211  }
1212  }
1213  static void gsort_cb(Fl_Widget*, void* w)
1214  {
1215  ((MainWindow*) w)->gsort_cb_i();
1216  }
1217 
1218  // Group select callback
1219  inline void gselect_cb_i(void)
1220  {
1221  groupSelect(UI_CB_START, groupList->value());
1222  }
1223  static void gselect_cb(Fl_Widget*, void* w)
1224  {
1225  ((MainWindow*) w)->gselect_cb_i();
1226  }
1227 
1228  // Next unread group callback
1229  inline void nug_cb_i(void)
1230  {
1231  int i;
1232  int groups = groupList->size();
1233  int current = groupList->value(); // Zero if no group is selected
1234  core_anum_t ur;
1235 
1236  wraparound:
1237  for(current ? i = current - 1 : i = 0; i < groups; ++i)
1238  {
1239  ur = groupGetUnreadNo(subscribedGroups[i].lwm,
1240  subscribedGroups[i].hwm, group_list[i].info);
1241  if(ur)
1242  {
1243  groupList->deselect();
1244  groupList->select(++i);
1245  groupSelect(UI_CB_START, i);
1246  break;
1247  }
1248  }
1249  if (1 < current)
1250  {
1251  groups = current - 1;
1252  current = 0;
1253  goto wraparound;
1254  }
1255  }
1256  static void nug_cb(Fl_Widget*, void* w)
1257  {
1258  ((MainWindow*) w)->nug_cb_i();
1259  }
1260 
1261  // Threaded view toggle callback
1262  // This callback should deactivate all menu items not usable in this mode
1263  inline void thrv_cb_i(void)
1264  {
1265  Fl_Menu_Item* mi = (Fl_Menu_Item*) menu->find_item(uthrv_sort_cb);
1266 
1267  if(config[CONF_TVIEW].val.i)
1268  {
1269  config[CONF_TVIEW].val.i = 0;
1270  if(NULL != mi) { mi->activate(); }
1271  }
1272  else
1273  {
1274  config[CONF_TVIEW].val.i = 1;
1275  if(NULL != mi) { mi->deactivate(); }
1276  }
1277  if(NULL != currentGroup) { updateTree(); }
1278 #if CFG_COCOA_SYS_MENUBAR
1279  menu->update();
1280 #endif // CFG_COCOA_SYS_MENUBAR
1281  }
1282  static void thrv_cb(Fl_Widget*, void* w)
1283  {
1284  ((MainWindow*) w)->thrv_cb_i();
1285  }
1286 
1287  // Unthreaded view sorting based on date or watermark toggle callback
1288  // Ignored for threaded view
1289  inline void uthrv_sort_cb_i(void)
1290  {
1291  if(config[CONF_UTVIEW_AN].val.i) { config[CONF_UTVIEW_AN].val.i = 0; }
1292  else { config[CONF_UTVIEW_AN].val.i = 1; }
1293  if(NULL != currentGroup) { updateTree(); }
1294  }
1295  static void uthrv_sort_cb(Fl_Widget*, void* w)
1296  {
1297  ((MainWindow*) w)->uthrv_sort_cb_i();
1298  }
1299 
1300  // Only unread flag toggle callback
1301  inline void onlyur_cb_i(void)
1302  {
1303  if(config[CONF_ONLYUR].val.i) { config[CONF_ONLYUR].val.i = 0; }
1304  else { config[CONF_ONLYUR].val.i = 1; }
1305  if(NULL != currentGroup) { updateTree(); }
1306  }
1307  static void onlyur_cb(Fl_Widget*, void* w)
1308  {
1309  ((MainWindow*) w)->onlyur_cb_i();
1310  }
1311 
1312  // Wrap text to fit width
1313  inline void wrap_cb_i(void)
1314  {
1315  if(Fl_Text_Display::WRAP_AT_BOUNDS != wrapMode)
1316  {
1317  wrapMode = Fl_Text_Display::WRAP_AT_BOUNDS;
1318  }
1319  else { wrapMode = Fl_Text_Display::WRAP_NONE; }
1320  text->wrap_mode(wrapMode, 0);
1321  }
1322  static void wrap_cb(Fl_Widget*, void* w)
1323  {
1324  ((MainWindow*) w)->wrap_cb_i();
1325  }
1326 
1327  // ROT13 encode/decode callback
1328  inline void rot13_cb_i(void);
1329  static void rot13_cb(Fl_Widget*, void* w)
1330  {
1331  ((MainWindow*) w)->rot13_cb_i();
1332  }
1333 
1334  // Mark single article unread callback
1335  // Now used to toggle (mark read if called again)
1336  inline void msau_cb_i(void);
1337  static void msau_cb(Fl_Widget*, void* w)
1338  {
1339  ((MainWindow*) w)->msau_cb_i();
1340  }
1341 
1342  // Mark subthread read callback
1343  inline void mssar(Fl_Tree_Item*);
1344  inline void mssar_cb_i(void);
1345  static void mssar_cb(Fl_Widget*, void* w)
1346  {
1347  ((MainWindow*) w)->mssar_cb_i();
1348  }
1349 
1350  // Mark all read callback
1351  inline void maar_cb_i(void);
1352  static void maar_cb(Fl_Widget*, void* w)
1353  {
1354  ((MainWindow*) w)->maar_cb_i();
1355  }
1356 
1357  // Mark (all articles in) all groups read callback
1358  inline void magar_cb_i(void);
1359  static void magar_cb(Fl_Widget*, void* w)
1360  {
1361  ((MainWindow*) w)->magar_cb_i();
1362  }
1363 
1364  // Article select callback
1365  inline void aselect_cb_i(void);
1366  static void aselect_cb(Fl_Widget*, void* w)
1367  {
1368  ((MainWindow*) w)->aselect_cb_i();
1369  }
1370 
1371  // View source code callback
1372  inline void viewsrc_cb_i(void)
1373  {
1374  viewSrc(UI_CB_START);
1375  }
1376  static void viewsrc_cb(Fl_Widget*, void* w)
1377  {
1378  ((MainWindow*) w)->viewsrc_cb_i();
1379  }
1380 
1381  // Compose callback
1382  inline void compose_cb_i(void)
1383  {
1384  articleCompose(false, false);
1385  }
1386  static void compose_cb(Fl_Widget*, void* w)
1387  {
1388  ((MainWindow*) w)->compose_cb_i();
1389  }
1390 
1391  // Reply callback
1392  inline void reply_cb_i(void)
1393  {
1394  articleCompose(true, false);
1395  }
1396  static void reply_cb(Fl_Widget*, void* w)
1397  {
1398  ((MainWindow*) w)->reply_cb_i();
1399  }
1400 
1401  // Cancel callback
1402  inline void cancel_cb_i(void)
1403  {
1404  articleCompose(false, true);
1405  }
1406  static void cancel_cb(Fl_Widget*, void* w)
1407  {
1408  ((MainWindow*) w)->cancel_cb_i();
1409  }
1410 
1411  // Supersede callback
1412  inline void supersede_cb_i(void)
1413  {
1414  articleCompose(true, true);
1415  }
1416  static void supersede_cb(Fl_Widget*, void* w)
1417  {
1418  ((MainWindow*) w)->supersede_cb_i();
1419  }
1420 
1421  // Reply by e-mail callback
1422  inline void email_cb_i(void)
1423  {
1424  sendEmail();
1425  }
1426  static void email_cb(Fl_Widget*, void* w)
1427  {
1428  ((MainWindow*) w)->email_cb_i();
1429  }
1430 
1431  // Next unread article callback
1432  inline void nua_cb_i(void)
1433  {
1434  std::size_t index = (std::size_t) groupList->value();
1435 
1436  if(!index)
1437  {
1438  SC("Do not use non-ASCII for the translation of this item")
1439  fl_message_title(S("Error"));
1440  fl_alert("%s", S("No group selected"));
1441  }
1442  else { ascrolldown_cb(false); }
1443  }
1444  static void nua_cb(Fl_Widget*, void* w)
1445  {
1446  ((MainWindow*) w)->nua_cb_i();
1447  }
1448 
1449  // Previous read article callback
1450  inline void pra_cb_i(void)
1451  {
1452  const char* mid;
1453  std::size_t len;
1454 
1455  if(NULL == lastArticleHE)
1456  {
1457  SC("Do not use non-ASCII for the translation of this item")
1458  fl_message_title(S("Error"));
1459  fl_alert("%s", S("No previously read article stored for group"));
1460  }
1461  else
1462  {
1463  mid = lastArticleHE->header->msgid;
1464  len = std::strlen(mid);
1465  // Use Message-ID without angle brackets
1466  if(NULL == searchSelectArticle(&mid[1], len - (std::size_t) 2))
1467  {
1468  SC("Do not use non-ASCII for the translation of this item")
1469  fl_message_title(S("Error"));
1470  fl_alert("%s", "Previous article not found (bug)");
1471  }
1472  }
1473  }
1474  static void pra_cb(Fl_Widget*, void* w)
1475  {
1476  ((MainWindow*) w)->pra_cb_i();
1477  }
1478 
1479  // Hyperlink clicked callback
1480  inline void hyperlink_cb_i(void)
1481  {
1482  hyperlinkHandler(hyperlinkPosition);
1483  }
1484  static void hyperlink_cb(Fl_Widget*, void* w)
1485  {
1486  ((MainWindow*) w)->hyperlink_cb_i();
1487  }
1488 
1489  void clearTree(void);
1490  void scrollTree(ui_scroll, Fl_Tree_Item*);
1491  bool checkTreeBranchForUnread(Fl_Tree_Item*);
1492  bool checkTreeBranchForItem(Fl_Tree_Item*, Fl_Tree_Item*);
1493  Fl_Tree_Item* addTreeNodes(Fl_Tree_Item*, core_hierarchy_element*);
1494  void updateTree(void);
1495  inline void collapseTree(void)
1496  {
1497  int i;
1498 
1499  for(i = 0; i < articleTree->root()->children(); ++i)
1500  {
1501  articleTree->root()->child(i)->close();
1502  }
1503  }
1504  void groupSubscribe(int);
1505  core_anum_t groupGetUnreadNo(core_anum_t, core_anum_t,
1506  struct core_range*);
1507  void groupListUpdateEntry(std::size_t);
1508  int groupStateMerge(void);
1509  void groupSelect(int, int);
1510  void groupCAC(core_groupdesc*);
1511  void groupListGetProposal(int);
1512  void updateArticleTree(int);
1513  void articleSelect(int);
1514  void viewMotd(int);
1515  void viewSrc(int);
1516  void stripAngleAddress(char*);
1517  void articleCompose(bool, bool);
1518  void sendEmail(void);
1519  Fl_Tree_Item* searchSelectArticle(const char*, std::size_t);
1520  void storeMIMEEntityToFile(const char*, const char*);
1521  void hyperlinkHandler(int);
1522  const char* printHeaderFields(struct core_article_header*);
1523  bool stateMachine(enum mainWindowEvent);
1524  public:
1525  void viewArticle(int, const char*);
1526  static void group_list_refresh_timer_cb(void* w)
1527  {
1528  ((MainWindow*) w)->groupListRefresh(UI_CB_START);
1529  if(0 < config[CONF_REFRESH_INTERVAL].val.i)
1530  {
1531  double iv = 60.0 * (double) config[CONF_REFRESH_INTERVAL].val.i;
1532 
1533  // Start timeout for configured refresh interval
1534  Fl::add_timeout(iv, group_list_refresh_timer_cb, (void*) w);
1535  }
1536  }
1537  static void serverconf_cb(void* w)
1538  {
1539  ((MainWindow*) w)->updateServer(UI_CB_CONTINUE);
1540  }
1541  static void subscribe_cb1(void* w)
1542  {
1543  ((MainWindow*) w)->groupSubscribe(UI_CB_CONTINUE);
1544  }
1545  static void subscribe_cb2(void* w)
1546  {
1547  ((MainWindow*) w)->groupSubscribe(UI_CB_FINISH);
1548  }
1549  static void groupproposal_cb(void* w)
1550  {
1551  ((MainWindow*) w)->groupListGetProposal(UI_CB_CONTINUE);
1552  }
1553  static void refresh_cb1(void* w)
1554  {
1555  ((MainWindow*) w)->groupListRefresh(UI_CB_CONTINUE);
1556  }
1557  static void refresh_cb2(void* w)
1558  {
1559  ((MainWindow*) w)->groupListRefresh(UI_CB_FINISH);
1560  }
1561  static void group_cb(void* w)
1562  {
1563  ((MainWindow*) w)->groupSelect(UI_CB_CONTINUE, 0);
1564  }
1565  static void overview_cb(void* w)
1566  {
1567  ((MainWindow*) w)->updateArticleTree(UI_CB_FINISH);
1568  }
1569  static void header_cb(void* w)
1570  {
1571  ((MainWindow*) w)->updateArticleTree(UI_CB_CONTINUE);
1572  }
1573  static void body_cb(void* w)
1574  {
1575  ((MainWindow*) w)->articleSelect(UI_CB_CONTINUE);
1576  }
1577  static void motd_cb(void* w)
1578  {
1579  ((MainWindow*) w)->viewMotd(UI_CB_CONTINUE);
1580  }
1581  static void article_cb(void* w)
1582  {
1583  ((MainWindow*) w)->viewArticle(UI_CB_CONTINUE, NULL);
1584  }
1585  static void src_cb(void* w)
1586  {
1587  ((MainWindow*) w)->viewSrc(UI_CB_CONTINUE);
1588  }
1589  static void post_cb(void* w)
1590  {
1591  ((MainWindow*) w)->articlePost(UI_CB_CONTINUE, NULL);
1592  }
1593  // Search in article content
1594  inline void asearch_cb_i(void);
1595  static void asearch_cb(Fl_Widget*, void* w)
1596  {
1597  ((MainWindow*) w)->asearch_cb_i();
1598  }
1599  void ascrolldown_cb(bool);
1600  void articlePost(int, const char*);
1601  inline void composeComplete(void)
1602  {
1603  if(!stateMachine(EVENT_COMPOSE_EXIT))
1604  {
1605  PRINT_ERROR("Error in main window state machine");
1606  }
1607  }
1608  void calculatePercent(std::size_t, std::size_t);
1609  inline int groupStateExport(void)
1610  {
1611  return(core_export_group_states(group_num, group_list));
1612  }
1613  void updateServer(int);
1614  void groupListRefresh(int);
1615  inline void groupListImport(void)
1616  {
1617  core_destroy_subscribed_group_states(&group_num, &group_list);
1618  groupListRefresh(UI_CB_START);
1619  }
1620  void articleUpdate(Fl_Text_Buffer*);
1621  inline int getTilingX(void) { return(groupList->w()); }
1622  inline int getTilingY(void) { return(articleTree->h()); }
1623  inline void setTilingX(int tx)
1624  {
1625  // Enforce some minimum width
1626  if(50 > tx) { tx = 50; }
1627  if(w() - 50 < tx) { tx = w() - 50; }
1628  contentGroup
1629 #if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1630  // Requires FLTK 1.4.0
1631  ->move_intersection
1632 #else // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1633  ->position
1634 #endif // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1635  (230, 0, tx, 0);
1636  }
1637  inline void setTilingY(int ty)
1638  {
1639  // Enforce some minimum height
1640  if(50 > ty) { ty = 50; }
1641  if(groupList->h() - 50 < ty) { ty = groupList->h() - 50; }
1642  contentGroup2
1643 #if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1644  // Requires FLTK 1.4.0
1645  ->move_intersection
1646 #else // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1647  ->position
1648 #endif // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1649  (1, contentGroup2->y() + 140,
1650  1, contentGroup2->y() + ty);
1651  }
1652  MainWindow(const char*);
1653  ~MainWindow(void);
1654 };
1655 
1656 
1657 // =============================================================================
1658 // Macros
1659 
1660 //! Replace message in main window status bar
1661 #define UI_STATUS(s) { if(mainWindow) { mainWindow->statusBar->label(s); } }
1662 
1663 //! Clear main window progress bar
1664 #define UI_READY() \
1665 { \
1666  if(mainWindow) \
1667  { \
1668  mainWindow->progressBar->value(0.0); \
1669  mainWindow->progressBar->label(""); \
1670  mainWindow->default_cursor(FL_CURSOR_DEFAULT); \
1671  mainWindow->busy = false; \
1672  } \
1673 }
1674 
1675 //! Display "Busy" in main window progress bar
1676 #define UI_BUSY() \
1677 { \
1678  if(mainWindow) \
1679  { \
1680  mainWindow->progressBar->value(0.0); \
1681  SC("This string must fit into the progress bar box") \
1682  mainWindow->progressBar->label(S("Busy")); \
1683  mainWindow->default_cursor(FL_CURSOR_WAIT); \
1684  mainWindow->busy = true; \
1685  } \
1686 }
1687 
1688 //! Update value of main window progress bar
1689 #define UI_PROGRESS(s, e) \
1690 { \
1691  if(mainWindow) \
1692  { \
1693  mainWindow->calculatePercent(s, e); \
1694  if(100.0 == progress_percent_value || !progress_skip_update) \
1695  { \
1696  mainWindow->progressBar->value(progress_percent_value); \
1697  mainWindow->progressBar->copy_label(progress_percent_label); \
1698  } \
1699  if(!mainWindow->busy) \
1700  { \
1701  mainWindow->default_cursor(FL_CURSOR_WAIT); \
1702  mainWindow->busy = true; \
1703  } \
1704  Fl::check(); \
1705  progress_skip_update = true; \
1706  Fl::add_timeout(0.1, progress_release_cb, this); \
1707  } \
1708 }
1709 
1710 
1711 // =============================================================================
1712 // Variables
1713 
1714 // Pixmaps
1715 extern "C"
1716 {
1717 #include "pixmaps/pixmaps.c"
1718 }
1719 #if USE_WINDOW_ICON
1720 static Fl_Pixmap pm_window_icon(xpm_window_icon); // Icon for main window
1721 #endif // USE_WINDOW_ICON
1722 static Fl_Pixmap pm_own(xpm_own); // Icon for own articles
1723 static Fl_Pixmap pm_reply_to_own(xpm_reply_to_own); // Icon for own articles
1724 static Fl_Pixmap pm_score_down(xpm_score_down); // Icon for negative score
1725 static Fl_Pixmap pm_score_up(xpm_score_up); // Icon for positive score
1726 
1727 static Fl_Text_Buffer* dummyTb;
1728 #if USE_WINDOW_ICON
1729 static Fl_RGB_Image mainIcon(&pm_window_icon);
1730 #endif // USE_WINDOW_ICON
1731 static MainWindow* mainWindow = NULL;
1732 static ProtocolConsole* protocolConsole = NULL;
1733 static int exitRequest = 0;
1734 static bool lockingInitialized = false;
1735 static int offset_correction_x;
1736 static int offset_correction_y;
1737 
1738 
1739 // =============================================================================
1740 // Set default font
1741 // This is for internal size calculations, changing this will not display the
1742 // widgets with a different font!
1743 
1744 static void gui_set_default_font(void)
1745 {
1746  fl_font(FL_HELVETICA, FL_NORMAL_SIZE);
1747 }
1748 
1749 
1750 // =============================================================================
1751 // Greeting message (displayed as the default article on startup)
1752 
1753 static const char* gui_greeting(void)
1754 {
1755  std::ostringstream greetingString;
1756  const char* s1;
1757  char* s2;
1758  std::size_t len;
1759 
1760  // Create greeting string
1761  if(!std::strcmp(CFG_NAME, "flnews"))
1762  {
1763  greetingString
1764  << " ______ ___\n"
1765  << " / ___/ / /\n"
1766  << " / / / /\n"
1767  << " __/ /_ / / _______ _______ ___ ___ _______\n"
1768  << " /_ __/ / / / ___ \\ / ___ \\ / / / / / _____\\\n"
1769  << " / / / / / / / / / /__/__/ / /_/|_/ / /__/_____\n"
1770  << " / / / / / / / / / /_____ / ___ / ______/ /\n"
1771  << "/__/ /__/ /__/ /__/ \\_______/ \\__/ /___/ \\_______/";
1772  }
1773  else { greetingString << CFG_NAME; }
1774  greetingString << "\n\n"
1775  << S("A fast and lightweight Usenet newsreader for Unix.") << "\n\n"
1776  << CFG_NAME << " "
1777  // Display credits to the authors of libraries we use
1778  << S("is based in part on the work of the following projects:") << "\n"
1779  << "- FLTK project (https://www.fltk.org/)" << "\n"
1780  << "- The Unicode\xC2\xAE Standard (https://home.unicode.org/)" << "\n"
1781  << " Unicode is a registered trademark of Unicode, Inc. in the" << "\n"
1782  << " United States and other countries" << "\n"
1783 #if CFG_USE_TLS
1784 # if CFG_USE_LIBRESSL
1785  << "- LibreSSL project (https://www.libressl.org/)" << "\n"
1786 # else // CFG_USE_LIBRESSL
1787  << "- OpenSSL project (https://www.openssl.org/)" << "\n"
1788 # endif // CFG_USE_LIBRESSL
1789  << " This product includes software developed by" << "\n"
1790  << " the OpenSSL Project for use in the OpenSSL Toolkit" << "\n"
1791  << " This product includes software written by" << "\n"
1792  << " Tim Hudson <tjh@cryptsoft.com>" << "\n"
1793  << " This product includes cryptographic software written by" << "\n"
1794  << " Eric Young <eay@cryptsoft.com>" << "\n"
1795 #endif // CFG_USE_TLS
1796 #if CFG_USE_ZLIB
1797  << "- zlib compression library (https://zlib.net/)" << "\n"
1798 #endif // CFG_USE_ZLIB
1799  << std::flush;
1800 
1801  // Assigning a name to the temporary string object forces it to stay in
1802  // memory as long as its name go out of scope (this is what we need).
1803  const std::string& gs = greetingString.str();
1804 
1805  // Allocate memory and copy greeting string
1806  s1 = gs.c_str();
1807  len = std::strlen(s1) + (std::size_t) 1;
1808  s2 = new char[len];
1809  std::memcpy(s2, s1, len);
1810 
1811  return(s2);
1812 }
1813 
1814 
1815 // =============================================================================
1816 // UTF-8 to ISO 8859-1 conversion for window manager
1817 // The caller is responsible to release the memory allocated for the result
1818 
1819 #if CFG_USE_XSI && !CFG_NLS_DISABLE
1820 static const char* gui_utf8_iso(const char* s)
1821 {
1822  const char* res = NULL;
1823  unsigned int len;
1824  char* tmp;
1825 
1826  // Check whether locale use UTF-8 (Decision based on 'LC_CTYPE')
1827  if(!fl_utf8locale())
1828  {
1829  // No => Expect that the WM can display ISO 8859-1
1830  //PRINT_ERROR("Non UTF-8 locale, converting string to ISO 8859-1");
1831  len = fl_utf8toa(s, (unsigned int) std::strlen(s), NULL, 0);
1832  tmp = new char[len + 1U];
1833  if(fl_utf8toa(s, (unsigned int) std::strlen(s), tmp, len + 1U) != len)
1834  {
1835  delete[] tmp;
1836  }
1837  else { res = (const char*) tmp; }
1838  }
1839  else
1840  {
1841  // Yes => Return string unchanged
1842  len = (unsigned int) std::strlen(s);
1843  tmp = new char[len + 1U];
1844  std::strncpy(tmp, s, len + 1U);
1845  res = (const char*) tmp;
1846  }
1847  // Return NULL if conversion has failed or is not supported
1848  return(res);
1849 }
1850 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
1851 
1852 
1853 // =============================================================================
1854 // Store content of FLTK text buffer to file
1855 // The encoding of the pathname is converted to the encoding of the locale
1856 // Returns zero on success
1857 
1858 
1859 static int gui_save_to_file(const char* pathname, Fl_Text_Buffer* tbuf)
1860 {
1861  int rv = -1;
1862  char* s = tbuf->text();
1863 
1864  if(NULL != s)
1865  {
1866  rv = core_save_to_file(pathname, s);
1867  std::free((void*) s);
1868  }
1869 
1870  return(rv);
1871 }
1872 
1873 
1874 // =============================================================================
1875 // Check whether article is suitable for display.
1876 //
1877 // FLTK 1.3 has problems with very long lines. The lines are not rendered
1878 // correctly (so that the horizontal scrollbar can be used). There are reports
1879 // that displaying such text have crashed the whole application.
1880 // This bug is fixed in FLTK 1.4.
1881 //
1882 // This function should check for problematic content and replace the data in
1883 // the text buffer tbuf with an appropriate error message.
1884 
1885 static void gui_check_article(Fl_Text_Buffer* tbuf)
1886 {
1887  int error = 0;
1888 
1889 #if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1890  // NOP
1891 #else // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1892  // Search for lines longer than 1000 octets (including CRLF).
1893  int len = tbuf->length();
1894  int pos = 0;
1895 
1896  while(len > pos)
1897  {
1898  int eol = tbuf->line_end(pos);
1899 
1900  if(1000 <= eol - pos)
1901  {
1902  // Line length error detected
1903  error = 1;
1904  break;
1905  }
1906 
1907  pos = eol;
1908  // Skip newline
1909  if(INT_MAX > pos) { ++pos; }
1910  }
1911 #endif // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1912 
1913  if(error)
1914  {
1915  // The delimiter is required for font selection and citation
1916  tbuf->text(ENC_DELIMITER);
1917  tbuf->append("[Error]\n");
1918  tbuf->append(S("Invalid line length"));
1919  }
1920 }
1921 
1922 
1923 // =============================================================================
1924 // Some FLTK backends always display SHY, others never display SHY.
1925 // The first variant is correct in the sense of ISO 8859-1, but is not
1926 // the intended behaviour of most users. The second variant is wrong,
1927 // because Unicode specifies that a SHY at the end of a line (where a
1928 // linebreak was inserted by rewrap algorithm) should be visible.
1929 // This should give consistent presentation of Soft Hyphen (SHY):
1930 // - Delete all SHY characters not at the end of a line
1931 // - Replace SHY at the end of a line with Hyphen-Minus
1932 
1933 static void gui_process_shy(Fl_Text_Buffer* tbuf)
1934 {
1935  int pos_shy = 0; // Search for soft hyphen from this position
1936  int rv, rv2;
1937 
1938  do
1939  {
1940  rv = tbuf->findchar_forward(pos_shy, 0x00ADU, &pos_shy);
1941  if(rv)
1942  {
1943  // Check next character
1944  rv2 = tbuf->next_char(pos_shy);
1945  if(0x000AU == tbuf->char_at(rv2))
1946  {
1947  // LF => Replace SHY with hyphen-minus (at end of line)
1948  tbuf->replace(pos_shy, rv2, "-");
1949  }
1950  else { tbuf->remove(pos_shy, rv2); }
1951  }
1952  }
1953  while(rv);
1954 }
1955 
1956 
1957 // =============================================================================
1958 // Check subject line for "Re: " and report position behind (for replies)
1959 // The caller must insert a "Re: " before that position to create a reply.
1960 
1961 static const char* gui_check_re_prefix(const char* subject)
1962 {
1963  bool prefix_replaced; // Nonstandard subject prefix replaced with "Re: "
1964 
1965  if((std::size_t) 3 <= std::strlen(subject))
1966  {
1967  // To be more tolerant, also accept "Re:", "RE:", "RE: ", "re:", "re: ",
1968  // "AW:", "AW: ", "Aw:" and "Aw: "
1969  do
1970  {
1971  prefix_replaced = false;
1972  if( (!std::strncmp(subject, "Re:", 3) && ' ' != subject[3])
1973  || !std::strncmp(subject, "RE:", 3)
1974  || !std::strncmp(subject, "re:", 3)
1975  || !std::strncmp(subject, "AW:", 3)
1976  || !std::strncmp(subject, "Aw:", 3) )
1977  {
1978  PRINT_ERROR("Nonstandard subject prefix"
1979  " replaced with \"Re: \"");
1980  subject = &subject[3];
1981  if(' ' == subject[0]) { subject = &subject[1]; }
1982  prefix_replaced = true;
1983  }
1984  }
1985  while(prefix_replaced);
1986  // Standard variant
1987  if(!std::strncmp(subject, "Re: ", 4)) { subject = &subject[4]; }
1988  }
1989 
1990  return(subject);
1991 }
1992 
1993 
1994 // =============================================================================
1995 // Create comma separated list of newsgroups
1996 // The names of the newsgroups must be passed as NULL-terminated 'array'.
1997 // The caller is responsible to delete[] the memory for the result string
1998 
1999 static const char* gui_create_newsgroup_list(const char** array)
2000 {
2001  static const char error[] = "[Error]";
2002  char* res = NULL;
2003  std::size_t size_max = (std::size_t) -1;
2004  std::size_t i = 0;
2005  std::size_t len = 0;
2006  std::size_t j;
2007 
2008  // Calculate required memory size
2009  while(NULL != array[i])
2010  {
2011  if(UINT_MAX == i) { break; }
2012  j = std::strlen(array[i]);
2013  if(size_max - len < j) { break; }
2014  len += j;
2015  ++i;
2016  }
2017  if(!i || (size_max - len < i))
2018  {
2019  res = new char[std::strlen(error) + (size_t) 1];
2020  std::strcat(res, error);
2021  }
2022  else
2023  {
2024  // Additional memory for comma separators and NUL termination
2025  len += i;
2026  // Create newsgroup list
2027  res = new char[len];
2028  res[0] = 0;
2029  for(j = 0; i > j; ++j)
2030  {
2031  if(j) { std::strcat(res, ","); }
2032  std::strcat(res, array[j]);
2033  }
2034  res[len - (std::size_t) 1] = 0;
2035  }
2036 
2037  return(res);
2038 }
2039 
2040 
2041 // =============================================================================
2042 // Check for signature separator to be the last one in MIME entity
2043 
2044 static int gui_last_sig_separator(char* buf)
2045 {
2046  int res = 1;
2047  char* p;
2048  char* q;
2049 
2050  // Attention: Overloaded and both prototypes different than in C!
2051  p = std::strstr(buf, "\n-- \n");
2052  if(NULL != p)
2053  {
2054  res = 0;
2055  // Check for MIME entity separator before next signature separator
2056  // Attention: Overloaded and both prototypes different than in C!
2057  q = std::strstr(buf, "\n________________________________________"
2058  "_______________________________________|\n");
2059  if(NULL != q && p > q) { res = 1; }
2060  }
2061 
2062  return(res);
2063 }
2064 
2065 
2066 // =============================================================================
2067 // Add introduction line and cite content (for replies)
2068 // The name of the cited author must be passed as 'ca'.
2069 // The array with newsgroup names of cited article must be passed as 'nga'.
2070 
2071 static void gui_cite_content(Fl_Text_Buffer* compText, const char* ca,
2072  const char** nga)
2073 {
2074  const char* ngl = gui_create_newsgroup_list(nga);
2075  const char* qm; // Quote mark
2076  int rv = 1;
2077  int pos = 0;
2078  int pos_found;
2079  bool fl = true; // Flag indicating first level citation
2080  unsigned int c;
2081  unsigned int next;
2082  const char* intro;
2083 
2084  // Set quote mark style according to config file
2085  switch(config[CONF_QUOTESTYLE].val.i)
2086  {
2087  case 0: { qm = ">"; break; }
2088  case 1: { qm = "> "; break; }
2089  default:
2090  {
2091  PRINT_ERROR("Quoting style configuration not supported");
2092  // Use default from old versions that can't be configured
2093  qm = "> ";
2094  break;
2095  }
2096  }
2097  // Quote original content
2098  if(compText->length())
2099  {
2100  while(compText->char_at(pos))
2101  {
2102  // Check for second level citation marks
2103  if((unsigned int) '>' == compText->char_at(pos))
2104  {
2105  fl = false;
2106  }
2107  // Add additional citation mark
2108  compText->insert(pos, qm);
2109  // Unify spacing between and after citation marks if configured
2110  if(config[CONF_QUOTEUNIFY].val.i)
2111  {
2112  if(!config[CONF_QUOTESTYLE].val.i)
2113  {
2114  do
2115  {
2116  c = compText->char_at(pos);
2117  next = compText->char_at(compText->next_char(pos));
2118  if((unsigned int) '>' == c)
2119  {
2120  if((unsigned int) ' ' == next)
2121  {
2122  // Remove space between marks: "> " => ">"
2123  compText->remove(pos + 1, pos + 2);
2124  }
2125  }
2126  pos = compText->next_char(pos);
2127  next = compText->char_at(pos);
2128  }
2129  while((unsigned int) '>' == next || (unsigned int) ' ' == next);
2130  }
2131  else
2132  {
2133  do
2134  {
2135  c = compText->char_at(pos);
2136  next = compText->char_at(compText->next_char(pos));
2137  if((unsigned int) '>' == c)
2138  {
2139  if((unsigned int) '>' == next)
2140  {
2141  // Add missing space between marks: ">>" => "> >"
2142  compText->insert(++pos, " ");
2143  }
2144  else if((unsigned int) '>' != next
2145  && (unsigned int) ' ' != next)
2146  {
2147  // Missing space after last mark "> >" => "> > "
2148  compText->insert(++pos, " ");
2149  }
2150  }
2151  pos = compText->next_char(pos);
2152  }
2153  while((unsigned int) '>' == next || (unsigned int) ' ' == next);
2154  }
2155  }
2156  // Search next EOL
2157  rv = compText->findchar_forward(pos, (unsigned int) '\n',
2158  &pos_found);
2159  if(!rv) { break; } else { pos = ++pos_found; }
2160  }
2161  }
2162  // Prepend new introduction line
2163  if(fl)
2164  {
2165  // Insert empty line between intro lines and cited content
2166  compText->insert(0, "\n"); compText->insert(0, qm);
2167  }
2168  // Prepend new introduction line with cited authors name
2169  intro = core_get_introduction(ca, ngl);
2170  if(NULL == intro) { compText->insert(0, "[Error] wrote:\n"); }
2171  else
2172  {
2173  compText->insert(0, "\n");
2174  compText->insert(0, intro);
2175  }
2176  core_free((void*) intro);
2177  delete[] ngl;
2178  // Insert empty line after citation
2179  compText->append("\n");
2180 }
2181 
2182 
2183 // =============================================================================
2184 // Print some header fields of article
2185 //
2186 // The caller is responsible to free the memory allocated for the returned
2187 // string (if the result is not NULL).
2188 
2189 #define UI_HDR_FIELDS (std::size_t) 14
2190 #define UI_HDR_PAD(s, n) { for(i = 0; i < n; ++i) { s << " "; } }
2191 static const char* gui_print_header_fields(struct core_article_header* h)
2192 {
2193  static const char* f[UI_HDR_FIELDS] =
2194  {
2195  S("Subject"),
2196  S("Date"),
2197  S("From"),
2198  S("Reply-To"),
2199  S("Newsgroups"),
2200  S("Followup-To"),
2201  S("Distribution"),
2202  S("Message-ID"),
2203  S("Supersedes"),
2204  S("Organization"),
2205  S("User-Agent"),
2206  S("Transfer-Encoding"),
2207  S("Content-Type"),
2208  S("References")
2209  };
2210  std::size_t flen[UI_HDR_FIELDS];
2211  std::size_t fpad[UI_HDR_FIELDS];
2212  std::ostringstream hdrData;
2213  char* s = NULL;
2214  std::size_t len;
2215  char date[20];
2216  int rv;
2217  std::size_t i;
2218  std::size_t largest = 0;
2219 
2220  // Calculate length of largest header field ID and padding for the others
2221  for(i = 0; i < UI_HDR_FIELDS; ++i)
2222  {
2223  len = std::strlen(f[i]);
2224 #if CFG_USE_XSI && !CFG_NLS_DISABLE
2225  // Attention: The header field names can be translated if NLS is enabled.
2226  // The resulting Unicode strings may have a different number of characters
2227  // than bytes!
2228  if(INT_MAX < len) { len = INT_MAX; }
2229  rv = fl_utf_nb_char((const unsigned char*) f[i], (int) len);
2230  if(0 > rv) { flen[i] = len; }
2231  else { flen[i] = (std::size_t) rv; }
2232 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
2233  // Not translated header field names are US-ASCII by definition
2234  flen[i] = len;
2235 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
2236  }
2237  for(i = 0; i < UI_HDR_FIELDS; ++i)
2238  {
2239  if(largest < flen[i]) { largest = flen[i]; }
2240  }
2241  for(i = 0; i < UI_HDR_FIELDS; ++i) { fpad[i] = largest - flen[i]; }
2242 
2243  // Copy header field content data
2244  UI_HDR_PAD(hdrData, fpad[0]);
2245  hdrData << f[0] << ": " << h->subject << "\n";
2246  rv = enc_convert_posix_to_iso8601(date, h->date);
2247  if(!rv)
2248  {
2249  UI_HDR_PAD(hdrData, fpad[1]);
2250  hdrData << f[1] << ": " << date << "\n";
2251  }
2252  UI_HDR_PAD(hdrData, fpad[2]);
2253  hdrData << f[2] << ": " << h->from << "\n";
2254  if(NULL != h->reply2)
2255  {
2256  if(std::strcmp(h->from, h->reply2))
2257  {
2258  UI_HDR_PAD(hdrData, fpad[3]);
2259  hdrData << f[3] << ": " << h->reply2 << "\n";
2260  }
2261  }
2262  UI_HDR_PAD(hdrData, fpad[4]);
2263  hdrData << f[4] << ": ";
2264  i = 0;
2265  while(NULL != h->groups[i])
2266  {
2267  if(i) { hdrData << ","; }
2268  hdrData << h->groups[i++];
2269  }
2270  hdrData << "\n";
2271  if(NULL != h->fup2)
2272  {
2273  UI_HDR_PAD(hdrData, fpad[5]);
2274  hdrData << f[5] << ": " << h->fup2 << "\n";
2275  }
2276 
2277  if(NULL != h->dist)
2278  {
2279  UI_HDR_PAD(hdrData, fpad[6]);
2280  hdrData << f[6] << ": " << h->dist << "\n";
2281  }
2282 
2283  UI_HDR_PAD(hdrData, fpad[7]);
2284  hdrData << f[7] << ": " << h->msgid << "\n";
2285  if(NULL != h->supers)
2286  {
2287  UI_HDR_PAD(hdrData, fpad[8]);
2288  hdrData << f[8] << ": " << h->supers << "\n";
2289  }
2290  if(NULL != h->org)
2291  {
2292  UI_HDR_PAD(hdrData, fpad[9]);
2293  hdrData << f[9] << ": " << h->org << "\n";
2294  }
2295  if(NULL != h->uagent)
2296  {
2297  UI_HDR_PAD(hdrData, fpad[10]);
2298  hdrData << f[10] << ": " << h->uagent << "\n";
2299  }
2300  else if(NULL != h->x_newsr)
2301  {
2302  UI_HDR_PAD(hdrData, fpad[10]);
2303  hdrData << f[10] << ": " << h->x_newsr << "\n";
2304  }
2305  else if(NULL != h->x_pagent)
2306  {
2307  UI_HDR_PAD(hdrData, fpad[10]);
2308  hdrData << f[10] << ": " << h->x_pagent << "\n";
2309  }
2310  else if(NULL != h->x_mailer)
2311  {
2312  UI_HDR_PAD(hdrData, fpad[10]);
2313  hdrData << f[10] << ": " << h->x_mailer << "\n";
2314  }
2315  if(NULL != h->mime_cte)
2316  {
2317  UI_HDR_PAD(hdrData, fpad[11]);
2318  hdrData << f[11] << ": " << h->mime_cte << "\n";
2319  }
2320  if(NULL != h->mime_ct)
2321  {
2322  UI_HDR_PAD(hdrData, fpad[12]);
2323  hdrData << f[12] << ": " << h->mime_ct << "\n";
2324  }
2325  if(NULL != h->refs)
2326  {
2327  UI_HDR_PAD(hdrData, fpad[13]);
2328  // The GS control character marks the beginning of the reference list
2329  // The syntax highlighting code should replace it with SP later
2330  hdrData << f[13] << ":" << "\x1D";
2331  i = 0;
2332  while(NULL != h->refs[i]) { hdrData << "[" << i++ << "]"; }
2333  hdrData << "\n";
2334  }
2335  hdrData << std::flush;
2336 
2337  // Attention:
2338  //
2339  // const char* hs = hdrData.str().c_str()
2340  //
2341  // creates 'hs' with undefined value because the compiler is allowed to
2342  // free the temporary string object returned by 'str()' immediately after
2343  // the assignment!
2344  // Assigning names to temporary string objects forces them to stay in
2345  // memory as long as their names go out of scope (this is what we need).
2346  const std::string& hs = hdrData.str();
2347 
2348  len = std::strlen(hs.c_str());
2349  s = new char[++len];
2350  std::strncpy(s, hs.c_str(), len);
2351 
2352  return(s);
2353 }
2354 
2355 
2356 // =============================================================================
2357 // Create RFC 8089 conformant link to save entity to a file
2358 //
2359 // We use a reserved subdirectory in CFG_NAME to avoid clashes with links
2360 // created by the author of the article.
2361 //
2362 // The parameter msgid must point to a string with a valid Message-ID.
2363 //
2364 // The caller is responsible to free() the memory allocated for the link if
2365 // the result is not NULL.
2366 
2367 static const char* gui_create_link_to_entity(const char* msgid,
2368  std::size_t entity)
2369 {
2370  const char* link = NULL;
2371  Fl_Text_Buffer tb;
2372  const char* confdir = NULL;
2373  char* msgid_buf;
2374  std::size_t len;
2375  std::ostringstream entity_no;
2376 
2377  confdir = xdg_get_confdir(CFG_NAME);
2378  if(NULL != confdir)
2379  {
2380  tb.text("<file://");
2381  tb.append(confdir);
2382  tb.append("/.mime/entities/");
2383  // Append Message-ID without angle brackets
2384  len = std::strlen(msgid);
2385  msgid_buf = new char[len];
2386  std::strncpy(msgid_buf, &msgid[1], --len);
2387  msgid_buf[--len] = 0;
2388  tb.append(msgid_buf);
2389  delete[] msgid_buf;
2390  tb.append("/");
2391  // Append entity index
2392  // Note: We cannot use 'posix_snprintf()' here
2393  entity_no.str(std::string());
2394  entity_no << entity << std::flush;
2395  const std::string& s = entity_no.str();
2396  tb.append(s.c_str());
2397  // Append closing delimiter
2398  tb.append(">");
2399  // Copy complete link
2400  link = tb.text();
2401  }
2402  core_free((void*) confdir);
2403 
2404  return(link);
2405 }
2406 
2407 
2408 // =============================================================================
2409 // Decode MIME entities into FLTK text buffer
2410 //
2411 // The parameter tb must be a valid pointer to a FLTK text buffer.
2412 // The parameter mimeData is allowed to be NULL.
2413 
2414 static void gui_decode_mime_entities(Fl_Text_Buffer *tb,
2415  MIMEContent* mimeData,
2416  const char* msgid)
2417 {
2418  const char* p = NULL;
2419  const char* q = NULL;
2420  std::size_t i;
2421  std::size_t first;
2422  std::size_t num;
2423  enc_mime_cte cte = ENC_CTE_UNKNOWN;
2424  enc_mime_ct* ct = NULL;
2425  const char* header;
2426  const char* body;
2427  char last_char;
2428  int attachment;
2429 
2430  if(NULL == mimeData)
2431  {
2432  tb->append(S("MIME content handling error"));
2433  }
2434  else
2435  {
2436  first = 0;
2437  num = mimeData->parts();
2438  // Append MIME entities to text buffer
2439  for(i = first; i < num; ++i)
2440  {
2441  body = mimeData->part(i, &cte, &ct);
2442  // Print delimiter between entities
2443  if(i)
2444  {
2445  // Append a linefeed if last entity doesn't has one at its end
2446  last_char = tb->byte_at(tb->length() - 1);
2447  if((char) 0x0A != last_char) { tb->append("\n"); }
2448  tb->append(ENC_DELIMITER);
2449  }
2450  // Print own headers for multipart entities
2451  if(mimeData->is_multipart())
2452  {
2453  header = mimeData->part_header(i);
2454  if(NULL != header)
2455  {
2456  tb->append(header);
2457  tb->append("\n");
2458  }
2459  }
2460  // Check content disposition
2461  attachment = 0;
2462  if(ENC_CD_ATTACHMENT == mimeData->type(i)) { attachment = 1; }
2463  if(attachment)
2464  {
2465  // Respect content disposition declaration "attachment"
2466  tb->append(S("Declared as attachment by sender"));
2467  // Display RFC 8089 conformant link to save entity to a file.
2468  tb->append("\n");
2469  p = gui_create_link_to_entity(msgid, i);
2470  if(NULL == p) { tb->append("[Error]"); }
2471  else { tb->append(p); }
2472  std::free((void*) p);
2473  }
2474  // RFC 2049 requires that unknown subtypes of "text" must be
2475  // viewable raw/undecoded => We handle them as subtype "plain"
2476  // (print them undecoded)
2477  else if(ENC_CT_TEXT != ct->type && ENC_CT_MESSAGE != ct->type)
2478  {
2479  // Error: Content is not supported
2480  tb->append(S("MIME content type not supported"));
2481  // Display RFC 8089 conformant link to save entity to a file.
2482  tb->append("\n");
2483  p = gui_create_link_to_entity(msgid, i);
2484  if(NULL == p) { tb->append("[Error]"); }
2485  else { tb->append(p); }
2486  std::free((void*) p);
2487  }
2488  else
2489  {
2490  // Decode 'text/plain' content and convert it to Unicode
2491  p = enc_mime_decode(cte, ct->charset, body);
2492  if(NULL == p)
2493  {
2494  tb->append(S("MIME content decoding failed"));
2495  }
2496  else
2497  {
2498  // Handle content with "Format=Flowed" attribute
2499  if(ct->flags & ENC_CT_FLAG_FLOWED)
2500  {
2501  q = enc_mime_flowed_decode(p,
2502  ct->flags & ENC_CT_FLAG_DELSP,
2503  ct->flags & ENC_CT_FLAG_INSLINE);
2504  if(NULL == q)
2505  {
2506  PRINT_ERROR("Decoding of MIME Format=Flowed"
2507  " content failed");
2508  }
2509  else
2510  {
2511  if(p != q && p != body) { enc_free((void*) p); }
2512  p = q;
2513  }
2514  }
2515  // Convert article content line breaks from canonical
2516  // (RFC 822) form to POSIX form
2517  q = core_convert_canonical_to_posix(p, 1, 0);
2518  if(NULL == q)
2519  {
2520  tb->append(
2521  S("Conversion from canonical to local form failed"));
2522  }
2523  else
2524  {
2525  // Free memory allocated by MIME decoder (if any)
2526  if(p != q && p != body) { enc_free((void*) p); }
2527  p = q;
2528  // Success
2529  tb->append(p);
2530  if(p != body) { enc_free((void*) p); }
2531  }
2532  }
2533  }
2534  }
2535  }
2536 }
2537 
2538 
2539 // =============================================================================
2540 // Check whether file exists and ask user if OK to overwrite
2541 
2542 static int gui_check_pathname(const char* pathname)
2543 {
2544  int res = 0;
2545  int rv;
2546 
2547  rv = core_check_file_exist(pathname);
2548  if (0 == rv)
2549  {
2550  // File exists
2551  fl_message_title(S("Warning"));
2552  rv = !fl_choice("%s", S("Cancel"),
2553  S("OK"), NULL,
2554  S("File exists\nReally continue?"));
2555  if(rv)
2556  {
2557  // User does not want to overwrite the file
2558  res = -1;
2559  }
2560  }
2561 
2562  return(res);
2563 }
2564 
2565 
2566 // =============================================================================
2567 // Populate group list
2568 
2569 static int gui_populate_group_list(const char* grouplist)
2570 {
2571  int res = 0;
2572  std::size_t len;
2573  char* buf;
2574  int rv;
2575  int pos = 0;
2576  int pos_new;
2577 
2578  if(NULL != grouplist)
2579  {
2580  len = std::strlen(grouplist);
2581  if(len)
2582  {
2583  buf = new char[len + (std::size_t) 1];
2584  while(1)
2585  {
2586  rv = std::sscanf(&grouplist[pos], "%s%n", buf, &pos_new);
2587  if (1 != rv || !pos_new) { break; }
2588  else
2589  {
2590  pos += pos_new;
2591  (void) core_subscribe_group(buf);
2592  // Ignore potential error and continue with next group
2593  }
2594  }
2595  delete[] buf;
2596  }
2597  }
2598 
2599  return(res);
2600 }
2601 
2602 
2603 // =============================================================================
2604 // My_Multi_Browser event handler
2605 
2606 int My_Multi_Browser::handle(int event)
2607 {
2608  int res = 0;
2609 
2610  if(FL_KEYUP == event)
2611  {
2612  // FLTK 1.4.0 does not draw the focus rectangle correctly
2613  if(Fl::event_key() == FL_Up)
2614  {
2615  redraw();
2616  res = 1;
2617  }
2618  }
2619  else if(FL_KEYDOWN == event)
2620  {
2621  if(Fl::event_key() == FL_Enter)
2622  {
2623  void* sel = selection();
2624 
2625  if (NULL != sel)
2626  {
2627  deselect();
2628  select_only(sel, 1);
2629  res = 1;
2630  }
2631  }
2632  // Space key
2633  else if(Fl::event_key() == 0x20)
2634  {
2635  // Ignore
2636  res = 1;
2637  }
2638  }
2639 
2640  // Other events go to the default handler
2641  if(!res) { res = Fl_Multi_Browser::handle(event); }
2642 
2643  return(res);
2644 }
2645 
2646 
2647 // =============================================================================
2648 // My_Text_Display event handler
2649 
2650 int My_Text_Display::handle(int event)
2651 {
2652  int res = 0;
2653  int button;
2654  int x, y;
2655  int pos;
2656  unsigned int style;
2657 
2658  switch(event)
2659  {
2660  case FL_KEYDOWN:
2661  {
2662  // Space key pressed
2663  if(Fl::event_key() == 0x20)
2664  {
2665  // Scroll down
2666  mainWindow->ascrolldown_cb(true);
2667  res = 1;
2668  }
2669  // Slash key pressed
2670  else if(!std::strcmp(Fl::event_text(), "/"))
2671  {
2672  mainWindow->asearch_cb_i();
2673  res = 1;
2674  }
2675  break;
2676  }
2677  case FL_MOVE:
2678  {
2679  // Mouse has moved inside the widget
2680  if(NULL != mStyleBuffer && 0 != mMaxsize)
2681  {
2682  x = Fl::event_x();
2683  y = Fl::event_y();
2684  pos = xy_to_position(x, y);
2685  style = mStyleBuffer->char_at(pos);
2686  if(NULL != mainWindow)
2687  {
2688  if(style == mainWindow->hyperlinkStyle)
2689  {
2690  // Mouse over hyperlink => Change mouse pointer to 'hand'
2691  fl_cursor(FL_CURSOR_HAND);
2692  }
2693  else { fl_cursor(FL_CURSOR_DEFAULT); }
2694  res = 1;
2695  }
2696  }
2697  break;
2698  }
2699  case FL_PUSH:
2700  case FL_RELEASE:
2701  {
2702  // Mouse button pressed or released
2703  if(NULL != mStyleBuffer && 0 != mMaxsize)
2704  {
2705  button = Fl::event_button();
2706  if(FL_LEFT_MOUSE == button)
2707  {
2708  // Left mouse button detected
2709  x = Fl::event_x();
2710  y = Fl::event_y();
2711  pos = xy_to_position(x, y);
2712  style = mStyleBuffer->char_at(pos);
2713  // Check whether click hit hyperlink
2714  if(NULL != mainWindow)
2715  {
2716  if(style == mainWindow->hyperlinkStyle)
2717  {
2718  if(FL_PUSH == event) { linkPushed = pos; }
2719  else if(pos == linkPushed)
2720  {
2721  // Click on hyperlink detected => Execute callback
2722  // std::printf("Click on hyperlink detected\n");
2723  linkPushed = -1;
2724  mainWindow->hyperlinkPosition = pos;
2725  res = 1;
2726  do_callback();
2727  break;
2728  }
2729  }
2730  }
2731  }
2732  if(FL_RELEASE == event) { linkPushed = -1; }
2733  }
2734  break;
2735  }
2736  default:
2737  {
2738  break;
2739  }
2740  }
2741  // Other events go to the default handler
2742  if(!res) { res = Fl_Text_Display::handle(event); }
2743 
2744  return(res);
2745 }
2746 
2747 
2748 // =============================================================================
2749 // My_Tree event handler
2750 //
2751 // Since 1.3.1 the old Fl_Tree widget provides a 'get_item_focus()' method.
2752 // Since 1.3.3 the new Fl_Tree widget provides a 'next_visible_item()' method.
2753 
2754 int My_Tree::handle(int event)
2755 {
2756  int res = 0;
2757 
2758  // Consume and ignore events while update of widget is in progress
2759  if(update_in_progress())
2760  {
2761  res = 1;
2762  }
2763  // Intercept key pressed events
2764  else switch(event)
2765  {
2766  case FL_KEYDOWN:
2767  {
2768  // Down (cursor) key
2769 #ifdef FL_ABI_VERSION
2770 # if 10303 <= FL_ABI_VERSION
2771  // Requires FLTK 1.3.3
2772  if(Fl::event_key() == FL_Down)
2773  {
2774  Fl_Tree_Item* ti = get_item_focus();
2775  if(NULL != ti && NULL == next_visible_item(ti, FL_Down))
2776  {
2777  res = 1;
2778  break;
2779  }
2780  }
2781  else
2782 # endif // 10303 <= FL_ABI_VERSION
2783 #endif // FL_ABI_VERSION
2784  // Enter key
2785  if(Fl::event_key() == FL_Enter)
2786  {
2787 #ifdef FL_ABI_VERSION
2788 # if 10301 <= FL_ABI_VERSION
2789  // Requires FLTK 1.3.1
2790  // Looks like there is no safe workaround to emulate with 1.3.0
2791  Fl_Tree_Item* ti = first_selected_item();
2792  if(NULL != ti) { deselect(ti); }
2793  select(get_item_focus(), 1);
2794 # endif // 10301 <= FL_ABI_VERSION
2795 #endif // FL_ABI_VERSION
2796  res = 1;
2797  break;
2798  }
2799  // Space key
2800  else if(Fl::event_key() == 0x20)
2801  {
2802  mainWindow->ascrolldown_cb(true);
2803  res = 1;
2804  break;
2805  }
2806  // Slash key pressed
2807  else if(!std::strcmp(Fl::event_text(), "/"))
2808  {
2809  mainWindow->asearch_cb_i();
2810  res = 1;
2811  break;
2812  }
2813  // No 'break' here is intended, use default handler if ignored
2814  }
2815  // FALLTHROUGH
2816  default:
2817  {
2818  res = Fl_Tree::handle(event);
2819  break;
2820  }
2821  }
2822 
2823  return(res);
2824 }
2825 
2826 
2827 // =============================================================================
2828 // Main window callback state machine
2829 //
2830 // This function is intended to check whether an operation is currently allowed
2831 // to execute.
2832 //
2833 // Every operation that use shared ressources (like the core thread) must call
2834 // this function before start of execution with \e operation set to a unique
2835 // operation ID associated with this operation.
2836 // This function returns \c true (and change internal state) if starting
2837 // execution of this operation is allowed at the moment, otherwise the operation
2838 // must not be started (and the internal state is preserved).
2839 //
2840 // Some operations call this function again with a different operation ID to
2841 // indicate completion (such indications are always accepted by this function).
2842 
2843 bool MainWindow::stateMachine(enum mainWindowEvent operation)
2844 {
2845  bool res = true;
2846 
2847  switch(mainState)
2848  {
2849  case STATE_READY:
2850  {
2851  switch(operation)
2852  {
2853  case EVENT_SUBSCRIBE:
2854  case EVENT_GL_REFRESH:
2855  case EVENT_A_VIEW:
2856  case EVENT_A_MAAR:
2857  case EVENT_A_MAGAR:
2858  case EVENT_SRC_VIEW:
2859  case EVENT_MOTD_VIEW:
2860  {
2861  mainState = STATE_MUTEX;
2862  break;
2863  }
2864  case EVENT_SERVER:
2865  {
2866  mainState = STATE_SERVER1;
2867  break;
2868  }
2869  case EVENT_G_SELECT:
2870  {
2871  mainState = STATE_GROUP;
2872  break;
2873  }
2874  case EVENT_A_PREPARE:
2875  {
2876  mainState = STATE_NEXT;
2877  break;
2878  }
2879  case EVENT_SCROLL_NEXT:
2880  {
2881  mainState = STATE_SCROLL;
2882  break;
2883  }
2884  case EVENT_COMPOSE:
2885  {
2886  mainState = STATE_COMPOSE;
2887  break;
2888  }
2889  case EVENT_SERVER_EXIT:
2890  case EVENT_G_SELECT_EXIT:
2891  case EVENT_GL_REFRESH_EXIT:
2892  case EVENT_SCROLL_NEXT_EXIT:
2893  {
2894  // Ignore
2895  break;
2896  }
2897  default:
2898  {
2899  // Reject all other operations
2900  res = false;
2901  break;
2902  }
2903  }
2904  break;
2905  }
2906  case STATE_MUTEX:
2907  {
2908  switch(operation)
2909  {
2910  case EVENT_SUBSCRIBE_EXIT:
2911  case EVENT_GL_REFRESH_EXIT:
2912  case EVENT_A_VIEW_EXIT:
2913  case EVENT_A_MAAR_EXIT:
2914  case EVENT_A_MAGAR_EXIT:
2915  case EVENT_SRC_VIEW_EXIT:
2916  case EVENT_MOTD_VIEW_EXIT:
2917  {
2918  mainState = STATE_READY;
2919  break;
2920  }
2921  default:
2922  {
2923  // Reject all other operations
2924  res = false;
2925  break;
2926  }
2927  }
2928  break;
2929  }
2930  case STATE_SERVER1:
2931  {
2932  switch(operation)
2933  {
2934  case EVENT_GL_PROPOSAL:
2935  {
2936  mainState = STATE_PROPOSAL;
2937  break;
2938  }
2939  case EVENT_GL_REFRESH:
2940  {
2941  mainState = STATE_SERVER2;
2942  break;
2943  }
2944  case EVENT_SERVER_EXIT:
2945  {
2946  mainState = STATE_READY;
2947  break;
2948  }
2949  default:
2950  {
2951  // Reject all other operations
2952  res = false;
2953  break;
2954  }
2955  }
2956  break;
2957  }
2958  case STATE_SERVER2:
2959  {
2960  switch(operation)
2961  {
2962  case EVENT_SERVER_EXIT:
2963  case EVENT_GL_PROPOSAL_EXIT:
2964  {
2965  // Ignore
2966  break;
2967  }
2968  case EVENT_GL_REFRESH_EXIT:
2969  {
2970  mainState = STATE_READY;
2971  break;
2972  }
2973  default:
2974  {
2975  // Reject all other operations
2976  res = false;
2977  break;
2978  }
2979  }
2980  break;
2981  }
2982  case STATE_PROPOSAL:
2983  {
2984  switch(operation)
2985  {
2986  case EVENT_SERVER_EXIT:
2987  {
2988  // Ignore
2989  break;
2990  }
2991  case EVENT_GL_PROPOSAL_EXIT:
2992  {
2993  mainState = STATE_READY;
2994  break;
2995  }
2996  case EVENT_GL_REFRESH:
2997  {
2998  mainState = STATE_SERVER2;
2999  break;
3000  }
3001  default:
3002  {
3003  // Reject all other operations
3004  res = false;
3005  break;
3006  }
3007  }
3008  break;
3009  }
3010  case STATE_GROUP:
3011  {
3012  switch(operation)
3013  {
3014  case EVENT_G_SELECT_EXIT:
3015  case EVENT_AT_REFRESH:
3016  {
3017  // Preserve state
3018  break;
3019  }
3020  case EVENT_AT_REFRESH_EXIT:
3021  {
3022  mainState = STATE_READY;
3023  break;
3024  }
3025  default:
3026  {
3027  // Reject all other operations
3028  res = false;
3029  break;
3030  }
3031  }
3032  break;
3033  }
3034  case STATE_SCROLL:
3035  {
3036  switch(operation)
3037  {
3038  case EVENT_A_PREPARE:
3039  {
3040  mainState = STATE_NEXT;
3041  break;
3042  }
3043  case EVENT_SCROLL_NEXT_EXIT:
3044  {
3045  mainState = STATE_READY;
3046  break;
3047  }
3048  default:
3049  {
3050  // Reject all other operations
3051  res = false;
3052  break;
3053  }
3054  }
3055  break;
3056  }
3057  case STATE_NEXT:
3058  {
3059  switch(operation)
3060  {
3061  case EVENT_A_SELECT:
3062  case EVENT_SCROLL_NEXT_EXIT:
3063  {
3064  // Preserve state
3065  break;
3066  }
3067  case EVENT_A_SELECT_EXIT:
3068  {
3069  mainState = STATE_READY;
3070  break;
3071  }
3072  default:
3073  {
3074  // Reject all other operations
3075  res = false;
3076  break;
3077  }
3078  }
3079  break;
3080  }
3081  case STATE_COMPOSE:
3082  {
3083  switch(operation)
3084  {
3085  case EVENT_POST:
3086  {
3087  mainState = STATE_POST;
3088  break;
3089  }
3090  case EVENT_COMPOSE_EXIT:
3091  {
3092  mainState = STATE_READY;
3093  break;
3094  }
3095  default:
3096  {
3097  // Reject all other operations
3098  res = false;
3099  break;
3100  }
3101  }
3102  break;
3103  }
3104  case STATE_POST:
3105  {
3106  switch(operation)
3107  {
3108  case EVENT_POST_EXIT:
3109  {
3110  mainState = STATE_COMPOSE;
3111  break;
3112  }
3113  default:
3114  {
3115  // Reject all other operations
3116  res = false;
3117  break;
3118  }
3119  }
3120  break;
3121  }
3122  default:
3123  {
3124  // This should never be executed, try to recover
3125  PRINT_ERROR("Invalid state detected by main window state machine");
3126  mainState = STATE_READY;
3127  res = false;
3128  break;
3129  }
3130  }
3131 
3132  return(res);
3133 }
3134 
3135 
3136 // =============================================================================
3137 // Main Window exit callback
3138 
3139 void MainWindow::exit_cb_i(void)
3140 {
3141  // Ignore escape key
3142  if(Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
3143  {
3144  return;
3145  }
3146 
3147  // Schedule exit request
3148  exitRequest = 1;
3149 }
3150 
3151 
3152 // =============================================================================
3153 // Main Window print callback
3154 
3155 void MainWindow::print_cb_i(void)
3156 {
3157  const Fl_Font font = FL_COURIER;
3158  const Fl_Fontsize fontsize = 14;
3159  const char line[] = "________________________________________"
3160  "________________________________________";
3161  int old_scrollbar_size = Fl::scrollbar_size();
3162  int res = 0;
3163  Fl_Printer printer;
3164  Fl_Text_Buffer tb;
3165  Fl_Text_Buffer sb;
3166  Fl_Text_Display canvas(0, 0, 100, 100);
3167  int lines;
3168  int pages = 1;
3169  int frompage, topage;
3170  int w; // Canvas width in points
3171  int h = 0; // Canvas height in points
3172  int lh; // Line hight in points
3173  int lpp = 0; // Lines per page
3174  int pw, ph; // Printable area size in points
3175  float sf; // Scale factor
3176  float tmp;
3177  int i;
3178  char* p;
3179  int x, y;
3180  int current_line = 1; // The first line is number 1
3181  int rv;
3182 
3183  if(NULL == currentGroup)
3184  {
3185  SC("Do not use non-ASCII for the translation of this item")
3186  fl_message_title(S("Error"));
3187  fl_alert("%s", S("No group selected"));
3188  }
3189  else if(NULL == currentArticleHE)
3190  {
3191  SC("Do not use non-ASCII for the translation of this item")
3192  fl_message_title(S("Error"));
3193  fl_alert("%s", S("No article selected"));
3194  }
3195  else
3196  {
3197  // Calculate width for 80 columns and height for ISO 216 A4 paper
3198  fl_font(font, fontsize);
3199  fl_measure(line, w = 0, lh = 0);
3200  if(0 >= w || 0 >= lh) { res = -1; }
3201  else { h = (int) std::ceil((double) w * std::sqrt(2.0)); }
3202  if(!res)
3203  {
3204  // Prepare text buffer
3205  p = text->buffer()->text();
3206  if(NULL != p) { tb.text(p); }
3207  std::free((void*) p);
3208  lines = tb.count_lines(0, tb.length());
3209  if(main_debug)
3210  {
3211  printf("%s: %sPrinting article with %d lines\n",
3212  CFG_NAME, MAIN_ERR_PREFIX, lines);
3213  }
3214  if (0 >= lines) { res = -1; }
3215  }
3216  if(!res)
3217  {
3218  // Prepare style buffer
3219  p = currentStyle->text();
3220  if(NULL != p) { sb.text(p); }
3221  std::free((void*) p);
3222  // Prepare canvas
3223  w += lh;
3224  h += lh;
3225  canvas.box(FL_NO_BOX);
3226  canvas.textfont(font);
3227  canvas.textsize(fontsize);
3228  canvas.resize(0, 0, w, h);
3229  canvas.buffer(&tb);
3230  canvas.highlight_data(&sb, styles, styles_len, 'A', NULL, NULL);
3231  // Calculate number of pages
3232  i = 0;
3233  while(canvas.position_to_xy(i, &x, &y))
3234  {
3235  if(!canvas.move_down()) { break; }
3236  i = canvas.insert_position();
3237  ++lpp;
3238  }
3239  if(lines > lpp)
3240  {
3241  // One page is not sufficient
3242  // Remove one line because the last one is maybe only partly visible
3243  --lpp;
3244  if(0 >= lpp) { res = -1; }
3245  }
3246  if(main_debug)
3247  {
3248  printf("%s: %sPrinting %d lines per page\n",
3249  CFG_NAME, MAIN_ERR_PREFIX, lpp);
3250  }
3251  // Calculate lines per page
3252  if(!res)
3253  {
3254  for(i = 0; i < lpp; ++i) { tb.append("\n"); } // For scrolling
3255  pages = lines / lpp;
3256  if(lines % lpp) { ++pages; }
3257  if(1 > pages) { res = -1; }
3258  }
3259  if(main_debug)
3260  {
3261  printf("%s: %sPrinting %d pages\n",
3262  CFG_NAME, MAIN_ERR_PREFIX, pages);
3263  }
3264  }
3265  // Setup printer
3266  if(!res)
3267  {
3268  res = printer.start_job(pages, &frompage, &topage);
3269  if(!res)
3270  {
3271  // Scrollbars of canvas are printed without this
3272  Fl::scrollbar_size(0);
3273  for(i = 1; i <= pages; ++i)
3274  {
3275  // Scroll down to next page
3276  canvas.scroll(current_line, 0);
3277  // Check whether page was selected
3278  if(i >= frompage && i <= topage)
3279  {
3280  // Scale page to printable area
3281  res = printer.start_page();
3282  if(!res)
3283  {
3284  res = printer.printable_rect(&pw, &ph);
3285  if(!res)
3286  {
3287  sf = (float) pw / (float) w;
3288  tmp = (float) ph / (float) h;
3289  if(std::fabs(tmp) < std::fabs(sf)) { sf = tmp; }
3290  // Scaling sets origin too
3291  printer.scale(sf);
3292  // Print page
3293  printer.print_widget((Fl_Widget*) &canvas);
3294  }
3295  rv = printer.end_page();
3296  if(!res) { res = rv; }
3297  }
3298  }
3299  // Check for error
3300  if(res) { break; }
3301  else
3302  {
3303  // Calculate vertical offset for next page
3304  current_line += lpp;
3305  }
3306  }
3307  printer.end_job();
3308  Fl::scrollbar_size(old_scrollbar_size);
3309  }
3310  }
3311  }
3312 
3313  if(res)
3314  {
3315  SC("Do not use non-ASCII for the translation of this item")
3316  fl_message_title(S("Error"));
3317  fl_alert("%s", S("Printing failed or aborted"));
3318  }
3319 }
3320 
3321 
3322 // =============================================================================
3323 // Main Window save cooked article (to file, UTF-8 encoded) callback
3324 
3325 void MainWindow::asave_cb_i(void)
3326 {
3327  const char* pathname;
3328  int rv;
3329 
3330  if(NULL == currentGroup)
3331  {
3332  SC("Do not use non-ASCII for the translation of this item")
3333  fl_message_title(S("Error"));
3334  fl_alert("%s", S("No group selected"));
3335  }
3336  else if(NULL == currentArticleHE)
3337  {
3338  SC("Do not use non-ASCII for the translation of this item")
3339  fl_message_title(S("Error"));
3340  fl_alert("%s", S("No article selected"));
3341  }
3342  else
3343  {
3344  SC("Do not use characters for the translation that cannot be")
3345  SC("converted to the ISO 8859-1 character set for this item.")
3346  SC("Leave the original string in place if in doubt.")
3347  const char* title = S("Save article");
3348  const char* suggested_pathname = core_suggest_pathname();
3349 
3350  fl_file_chooser_ok_label(S("Save"));
3351  if(NULL != suggested_pathname)
3352  {
3353  pathname = fl_file_chooser(title, "*", suggested_pathname, 0);
3354  if(NULL != pathname)
3355  {
3356  // Ask user for overwrite permission if file exists
3357  rv = gui_check_pathname(pathname);
3358  if (0 == rv)
3359  {
3360  rv = gui_save_to_file(pathname, currentArticle);
3361  if(rv)
3362  {
3363  SC("Do not use non-ASCII for the translation of this item")
3364  fl_message_title(S("Error"));
3365  fl_alert("%s", S("Operation failed"));
3366  }
3367  }
3368  }
3369  core_free((void*) suggested_pathname);
3370  }
3371  }
3372 }
3373 
3374 
3375 // =============================================================================
3376 // Main Window search in article content
3377 
3378 void MainWindow::asearch_cb_i(void)
3379 {
3380  SearchWindow* sw = NULL;
3381  int result = -1;
3382  int found = 0;
3383  int found_pos = 0;
3384  std::size_t tmp, tmp2;
3385  char* p;
3386  int start, end, len;
3387  int sel_start, sel_end;
3388  SC("")
3389  SC("Do not use characters for the translation that cannot be converted to")
3390  SC("the ISO 8859-1 character set for this item.")
3391  SC("Leave the original string in place if in doubt.")
3392  const char* titleString = S("Search in article");
3393  SC("")
3394 
3395  if(NULL == currentArticleHE)
3396  {
3397  SC("Do not use non-ASCII for the translation of this item")
3398  fl_message_title(S("Error"));
3399  fl_alert("%s", S("No article selected"));
3400  }
3401  else
3402  {
3403  // Display "Search in article" window
3404 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3405  // Convert window title to the encoding required by the window manager (WM)
3406  // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
3407  // window titles.
3408  const char* title;
3409  title = gui_utf8_iso(titleString);
3410  if(NULL != title)
3411  {
3412  sw = new SearchWindow(title, &currentSearchString);
3413  delete[] title;
3414  }
3415 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3416  sw = new SearchWindow(titleString, &currentSearchString);
3417 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3418 
3419  // Wait for user to press OK or Cancel
3420 search_again:
3421  if(NULL != sw)
3422  {
3423  sw->default_cursor(FL_CURSOR_DEFAULT);
3424  UI_READY();
3425  sw->finished = 0;
3426  while(!sw->finished) { Fl::wait(); }
3427  sw->default_cursor(FL_CURSOR_WAIT);
3428  result = sw->finished;
3429  }
3430 
3431  // Check result
3432  currentArticle->unhighlight();
3433  if(0 > result)
3434  {
3435  // Search cancelled by user
3436  UI_STATUS(S("Search cancelled, reset start position."));
3437  currentSearchPosition = 0;
3438  }
3439  else
3440  {
3441  // Search in current article
3442  UI_STATUS(S("Searching in article ..."));
3443  UI_BUSY();
3444  Fl::check();
3445  if(config[CONF_SEARCH_CASE_IS].val.i)
3446  {
3447  // Case insensitive (using FLTK will not be Unicode conformant)
3448  p = currentArticle->text();
3449  found = !enc_uc_search(p, (size_t) currentSearchPosition,
3450  currentSearchString, &tmp, &tmp2);
3451  if((std::size_t) INT_MAX <= tmp) { found_pos = INT_MAX; }
3452  else { found_pos = (int) tmp; }
3453  if((std::size_t) INT_MAX <= tmp2) { len = INT_MAX; }
3454  else { len = (int) tmp2; }
3455  std::free((void*) p);
3456  }
3457  else
3458  {
3459  // Case sensitive (using FLTK)
3460  found = currentArticle->search_forward(currentSearchPosition,
3461  currentSearchString,
3462  &found_pos, 1);
3463  // Length was already checked for INT_MAX overflow by SearchWindow
3464  len = (int) std::strlen(currentSearchString);
3465  }
3466  if(found)
3467  {
3468  // Highlight found text
3469  start = found_pos;
3470  if(INT_MAX - start < len) { len = INT_MAX - start; }
3471  end = start + len;
3472  currentSearchPosition = end;
3473  currentArticle->highlight(start, end);
3474  UI_STATUS(S("Search string found and marked, position stored."));
3475  // Scroll vertically to bring found position into view
3476  //if(currentArticle->selection_position(&sel_start, &sel_end))
3477  if(currentArticle->highlight_position(&sel_start, &sel_end))
3478  {
3479  // Now: Horizontal scrolling doesn't work correctly without this
3480  text->insert_position(0);
3481  text->show_insert_position();
3482  text->insert_position(sel_end);
3483  text->show_insert_position();
3484  }
3485  }
3486  else
3487  {
3488  UI_STATUS(S("Search string not found, reset start position."));
3489  // Reset start position
3490  currentSearchPosition = 0;
3491  }
3492  // Return to search window
3493  goto search_again;
3494  }
3495 
3496  // Destroy search window
3497  if(NULL != sw) { delete sw; }
3498  UI_READY();
3499  }
3500 }
3501 
3502 
3503 // =============================================================================
3504 // Main window calculate percent value and label for progress bar
3505 
3506 void MainWindow::calculatePercent(std::size_t current, std::size_t complete)
3507 {
3508  std::ostringstream percentString;
3509 
3510  progress_percent_value = 100.0;
3511  progress_percent_label[0] = '1';
3512  progress_percent_label[1] = '0';
3513  progress_percent_label[2] = '0';
3514  progress_percent_label[3] = '%';
3515  if(complete && (current < complete))
3516  {
3517  progress_percent_value = (float) current / (float) complete;
3518  progress_percent_value *= (float) 100.0;
3519  percentString.precision(0);
3520  percentString << std::fixed << progress_percent_value << "%"
3521  << std::flush;
3522 
3523  // Attention:
3524  //
3525  // const char* s = percentString.str().c_str()
3526  //
3527  // creates 's' with undefined value because the compiler is allowed to
3528  // free the temporary string object returned by 'str()' immediately after
3529  // the assignment!
3530  // Assigning names to temporary string objects forces them to stay in
3531  // memory as long as their names go out of scope (this is what we need).
3532  const std::string& ps = percentString.str();
3533 
3534  std::strncpy(progress_percent_label, ps.c_str(), (std::size_t) 4);
3535  }
3536  // Terminate string
3537  progress_percent_label[4] = 0;
3538 }
3539 
3540 
3541 // =============================================================================
3542 // Main Window configuration callback
3543 
3544 void MainWindow::config_cb_i(void)
3545 {
3546  SC("")
3547  SC("Do not use characters for the translation that cannot be converted to")
3548  SC("the ISO 8859-1 character set for this item.")
3549  SC("Leave the original string in place if in doubt.")
3550  const char* titleString = S("Configuration");
3551  SC("")
3552 
3553  // Display "Configuration" window
3554 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3555  // Convert window title to the encoding required by the window manager (WM)
3556  // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
3557  // window titles.
3558  const char* title;
3559  title = gui_utf8_iso(titleString);
3560  if(NULL != title)
3561  {
3562  new MiscCfgWindow(title);
3563  delete[] title;
3564  }
3565 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3566  new MiscCfgWindow(titleString);
3567 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3568 }
3569 
3570 
3571 // =============================================================================
3572 // Main Window identity configuration callback
3573 
3574 void MainWindow::identity_cb_i(void)
3575 {
3576  SC("")
3577  SC("Do not use characters for the translation that cannot be converted to")
3578  SC("the ISO 8859-1 character set for this item.")
3579  SC("Leave the original string in place if in doubt.")
3580  const char* titleString = S("Identity configuration");
3581  SC("")
3582 
3583  // Display "Identity configuration" window
3584 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3585  // Convert window title to the encoding required by the window manager (WM)
3586  // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
3587  // window titles.
3588  const char* title;
3589  title = gui_utf8_iso(titleString);
3590  if(NULL != title)
3591  {
3592  new IdentityCfgWindow(title);
3593  delete[] title;
3594  }
3595 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3596  new IdentityCfgWindow(titleString);
3597 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3598 }
3599 
3600 
3601 // =============================================================================
3602 // Main Window Message-ID search callback
3603 
3604 void MainWindow::mid_search_cb_i(void)
3605 {
3606  SC("")
3607  SC("Do not use characters for the translation that cannot be converted to")
3608  SC("the ISO 8859-1 character set for this item.")
3609  SC("Leave the original string in place if in doubt.")
3610  const char* titleString = S("Message-ID search");
3611  SC("")
3612 
3613  // Display "Message-ID search" window
3614 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3615  // Convert window title to the encoding required by the window manager (WM)
3616  // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
3617  // window titles.
3618  const char* title;
3619  title = gui_utf8_iso(titleString);
3620  if(NULL != title)
3621  {
3622  new MIDSearchWindow(title);
3623  delete[] title;
3624  }
3625 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3626  new MIDSearchWindow(titleString);
3627 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3628 }
3629 
3630 
3631 // =============================================================================
3632 // Main Window about information callback
3633 
3634 void MainWindow::about_cb_i(void)
3635 {
3636  std::ostringstream titleString;
3637 
3638  // Create Unicode title for about window
3639  SC("")
3640  SC("Do not use characters for the translation that cannot be converted to")
3641  SC("the ISO 8859-1 character set for this item.")
3642  SC("Leave the original string in place if in doubt.")
3643  titleString << S("About") << " " << CFG_NAME << std::flush;
3644  SC("")
3645 
3646  // Attention:
3647  //
3648  // const char* title = titleString.str().c_str()
3649  //
3650  // creates 'title' with undefined value because the compiler is allowed to
3651  // free the temporary string object returned by 'str()' immediately after the
3652  // assignment!
3653  // Assigning names to temporary string objects forces them to stay in memory
3654  // as long as their names go out of scope (this is what we need).
3655  const std::string& ts = titleString.str();
3656  const std::string& as = aboutString.str();
3657 
3658  // Set "About" window title
3659 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3660  // Convert window title to the encoding required by the window manager (WM)
3661  // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
3662  // window titles.
3663  const char* title;
3664  title = gui_utf8_iso(ts.c_str());
3665  if(NULL != title)
3666  {
3667  fl_message_title(title);
3668  delete[] title;
3669  }
3670 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3671  fl_message_title(ts.c_str());
3672 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3673 
3674  // Display "About" window
3675  fl_message("%s", as.c_str());
3676 }
3677 
3678 
3679 // =============================================================================
3680 // Main Window bug report callback
3681 
3682 void MainWindow::bug_cb_i()
3683 {
3684  std::ostringstream subjectString;
3685  std::ostringstream configString;
3686  std::ostringstream contentString;
3687  std::ostringstream titleString;
3688  int rv = -1;
3689  char* maintainer;
3690  std::size_t len;
3691  char* p;
3692  char* q;
3693 
3694  configString << "Configuration:" << "\n"
3695  << CFG_NAME << " " << CFG_VERSION
3696  << " " << "for" << " " << CFG_OS << "\n"
3697 #if CFG_MODIFIED
3698  << "(This is a modified version!)" << "\n"
3699 #endif // CFG_MODIFIED
3700  << "Unicode version: " << UC_VERSION << "\n"
3701  << "NLS: "
3702 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3703  << "Enabled" << "\n"
3704 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3705  << "Disabled" << "\n"
3706 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3707  << "Message locale: " << nls_loc << "\n"
3708  << "TLS: "
3709 #if CFG_USE_TLS
3710  << "Available" << "\n"
3711 # if CFG_USE_OPENSSL_API_1_1
3712  << "Compiled for OpenSSL API 1.1" << "\n"
3713 # endif // !CFG_USE_OPENSSL_API_1_1
3714 #else // CFG_USE_TLS
3715  << "Not available" << "\n"
3716 #endif // CFG_USE_TLS
3717  << "Compiled for: FLTK " << FL_MAJOR_VERSION << "."
3718  << FL_MINOR_VERSION << "." << FL_PATCH_VERSION << "\n"
3719 #if CFG_CMPR_DISABLE
3720  << "Compression disabled" << "\n"
3721 #else // CFG_CMPR_DISABLE
3722  << "Compression available"
3723 # if CFG_USE_ZLIB
3724  << " (zlib)"
3725 # endif // CFG_USE_ZLIB
3726  << "\n"
3727 #endif // CFG_CMPR_DISABLE
3728  << "Build: " << BDATE
3729  << "\n\n"
3730  << "Problem description (in english):" << "\n\n"
3731  << std::flush;
3732  subjectString << "[" << CFG_NAME << "] "
3733  << "Bug report (...)"
3734  << std::flush;
3735  contentString << S("Report bug to:") << "\n"
3736  << CFG_MAINTAINER
3737  << "\n\n"
3738  << S("Use the following subject line and replace")
3739  << " '...' " << S("with a summary") << ":\n"
3740  << subjectString.str() << "\n\n"
3741  << S("Use the following skeleton:") << "\n"
3742  << "------------------------------------------------------\n"
3743  << configString.str()
3744  << "------------------------------------------------------\n"
3745  << std::flush;
3746 
3747  // Create Unicode title for bug window
3748  SC("")
3749  SC("Do not use characters for the translation that cannot be converted to")
3750  SC("the ISO 8859-1 character set for this item.")
3751  SC("Leave the original string in place if in doubt.")
3752  titleString << S("Bug report") << std::flush;
3753  SC("")
3754 
3755  // Attention:
3756  //
3757  // const char* title = titleString.str().c_str()
3758  //
3759  // creates 'title' with undefined value because the compiler is allowed to
3760  // free the temporary string object returned by 'str()' immediately after
3761  // the assignment!
3762  // Assigning names to temporary string objects forces them to stay in
3763  // memory as long as their names go out of scope (this is what we need).
3764  const std::string& ts = titleString.str();
3765  const std::string& cs = contentString.str();
3766  const std::string& config = configString.str();
3767  const std::string& subject = subjectString.str();
3768 
3769  // Try to start external e-mail handler
3770  len = std::strlen(CFG_MAINTAINER);
3771  p = new char[++len];
3772  std::strncpy(p, CFG_MAINTAINER, len);
3773  p[len - (size_t) 1] = 0; // Not needed, only to silence 'cppcheck'
3774  // Attention: Overloaded and both prototypes different than in C!
3775  q = std::strstr(p, "mailto:");
3776  if(NULL != q)
3777  {
3778  maintainer = &q[7];
3779  rv = enc_percent_decode(maintainer, 1);
3780  if(0 <= rv)
3781  {
3782  rv = ext_handler_email(maintainer, subject.c_str(), config.c_str());
3783  }
3784  }
3785  delete[] p;
3786  if(rv)
3787  {
3788  // Failed => Display information in own window
3789 
3790  // Set "Bug report" window title
3791 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3792  // Convert window title to encoding required by the window manager (WM)
3793  // It is assumed that the WM can display either ISO 8859-1 or UTF-8
3794  // encoded window titles.
3795  const char* title;
3796  title = gui_utf8_iso(ts.c_str());
3797  if(NULL != title)
3798  {
3799  // Display "Bug report" window
3800  new BugreportWindow(title, cs.c_str());
3801  // Release memory for window title
3802  delete[] title;
3803  }
3804 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3805  // Display "Bug report" window
3806  new BugreportWindow(ts.c_str(), cs.c_str());
3807 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3808  }
3809 }
3810 
3811 
3812 // =============================================================================
3813 // Main Window license information callback
3814 
3815 void MainWindow::license_cb_i(void)
3816 {
3817  std::ostringstream titleString;
3818 
3819  // Create Unicode title for license window
3820  SC("")
3821  SC("Do not use characters for the translation that cannot be converted to")
3822  SC("the ISO 8859-1 character set for this item.")
3823  SC("Leave the original string in place if in doubt.")
3824  titleString << S("License") << std::flush;
3825  SC("")
3826 
3827  // Attention:
3828  //
3829  // const char* title = titleString.str().c_str()
3830  //
3831  // creates 'title' with undefined value because the compiler is allowed to
3832  // free the temporary string object returned by 'str()' immediately after the
3833  // assignment!
3834  // Assigning names to temporary string objects forces them to stay in memory
3835  // as long as their names go out of scope (this is what we need).
3836  const std::string& ts = titleString.str();
3837 
3838  // Set "License" window title
3839 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3840  // Convert window title to the encoding required by the window manager (WM)
3841  // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
3842  // window titles.
3843  const char* title;
3844  title = gui_utf8_iso(ts.c_str());
3845  if(NULL != title)
3846  {
3847  // Display "License" window
3848  new LicenseWindow(title);
3849  // Release memory for window title
3850  delete[] title;
3851  }
3852 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3853  // Display "License" window
3854  new LicenseWindow(ts.c_str());
3855 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3856 }
3857 
3858 
3859 // =============================================================================
3860 // Main Window protocol console callback
3861 
3862 void MainWindow::console_cb_i(void)
3863 {
3864  if(NULL == protocolConsole)
3865  {
3866  SC("")
3867  SC("Do not use characters for the translation that cannot be converted to")
3868  SC("the ISO 8859-1 character set for this item.")
3869  SC("Leave the original string in place if in doubt.")
3870  protocolConsole = new ProtocolConsole(S("Protocol console"));
3871  SC("")
3872  }
3873 }
3874 
3875 
3876 // =============================================================================
3877 // Main Window ROT13 encode/decode callback
3878 
3879 void MainWindow::rot13_cb_i(void)
3880 {
3881  char* olddata;
3882  Fl_Text_Buffer* newdata = new Fl_Text_Buffer(0, 0);
3883 
3884  if(NULL == currentGroup)
3885  {
3886  SC("Do not use non-ASCII for the translation of this item")
3887  fl_message_title(S("Error"));
3888  fl_alert("%s", S("No group selected"));
3889  }
3890  else if(NULL == currentArticleHE)
3891  {
3892  SC("Do not use non-ASCII for the translation of this item")
3893  fl_message_title(S("Error"));
3894  fl_alert("%s", S("No article selected"));
3895  }
3896  else
3897  {
3898  // ROT13 should not be applied twice
3899  // The clickable references will not work anymore in this case
3900  if(rot13)
3901  {
3902  // Reload article
3903  aselect_cb_i();
3904  rot13 = false;
3905  }
3906  else
3907  {
3908  // Apply ROT13
3909  olddata = currentArticle->text();
3910  if(NULL != olddata)
3911  {
3912  enc_rot13(olddata);
3913  newdata->text(olddata);
3914  articleUpdate(newdata);
3915  }
3916  std::free((void*) olddata);
3917  rot13 = true;
3918  }
3919  }
3920 }
3921 
3922 
3923 // =============================================================================
3924 // Main Window mark single article unread callback
3925 // Now used to toggle (mark read if called again)
3926 
3927 void MainWindow::msau_cb_i(void)
3928 {
3929  Fl_Tree_Item* ti;
3930  core_anum_t a;
3931 
3932  if(NULL == currentGroup)
3933  {
3934  SC("Do not use non-ASCII for the translation of this item")
3935  fl_message_title(S("Error"));
3936  fl_alert("%s", S("No group selected"));
3937  }
3938  else if(NULL == currentArticleHE)
3939  {
3940  SC("Do not use non-ASCII for the translation of this item")
3941  fl_message_title(S("Error"));
3942  fl_alert("%s", S("No article selected"));
3943  }
3944  else
3945  {
3946  ti = articleTree->first_selected_item();
3947  a = ((core_hierarchy_element*) ti->user_data())->anum;
3948  // Check whether article was already read
3949  if(core_check_already_read(&group_list[group_list_index],
3950  (core_hierarchy_element*) ti->user_data()))
3951  {
3952  // Yes => Mark unread
3953  ti->labelfont(FL_HELVETICA_BOLD);
3954  core_mark_as_unread(&group_list[group_list_index], a);
3955  UI_STATUS(S("Marked article unread."));
3956  }
3957  else
3958  {
3959  // No => Mark read
3960  ti->labelfont(FL_HELVETICA);
3961  core_mark_as_read(&group_list[group_list_index], a);
3962  UI_STATUS(S("Marked article read."));
3963  }
3964  articleTree->redraw();
3965  groupListUpdateEntry(group_list_index);
3966  }
3967 }
3968 
3969 
3970 // =============================================================================
3971 // Main Window mark subthread read callback
3972 
3973 void MainWindow::mssar(Fl_Tree_Item* ti)
3974 {
3975  core_anum_t a;
3976  int i;
3977 
3978  a = ((core_hierarchy_element*) ti->user_data())->anum;
3979  // Check whether article was already read
3980  if(!core_check_already_read(&group_list[group_list_index],
3981  (core_hierarchy_element*) ti->user_data()))
3982  {
3983  // No => Mark read
3984  ti->labelfont(FL_HELVETICA);
3985  core_mark_as_read(&group_list[group_list_index], a);
3986  }
3987  // Process children recursively
3988  for(i = 0; ti->children() > i; ++i)
3989  {
3990  mssar(ti->child(i));
3991  }
3992 }
3993 
3994 
3995 void MainWindow::mssar_cb_i(void)
3996 {
3997  Fl_Tree_Item* ti;
3998 
3999  if(NULL == currentGroup)
4000  {
4001  SC("Do not use non-ASCII for the translation of this item")
4002  fl_message_title(S("Error"));
4003  fl_alert("%s", S("No group selected"));
4004  }
4005  else
4006  {
4007  ti = articleTree->first_selected_item();
4008  if(NULL == currentArticleHE || NULL == ti)
4009  {
4010  SC("Do not use non-ASCII for the translation of this item")
4011  fl_message_title(S("Error"));
4012  fl_alert("%s", S("No article selected"));
4013  }
4014  else
4015  {
4016  // Process all article tree nodes in subthread
4017  // Starting at current article
4018  mssar(ti);
4019 
4020  // Update article tree
4021  updateTree();
4022 
4023  // Close subtree (now marked read)
4024  ti = articleTree->first_selected_item();
4025  if(NULL != ti)
4026  {
4027  articleTree->close(ti);
4028  }
4029 
4030  // Update group list
4031  groupListUpdateEntry(group_list_index);
4032  UI_STATUS(S("Marked all articles in subthread read."));
4033  }
4034  }
4035 }
4036 
4037 
4038 // =============================================================================
4039 // Main Window mark all read callback
4040 
4041 void MainWindow::maar_cb_i(void)
4042 {
4043  Fl_Tree_Item* ti;
4044  core_anum_t a;
4045 
4046  if(stateMachine(EVENT_A_MAAR))
4047  {
4048  if(NULL != currentGroup)
4049  {
4050  // Process all article tree nodes (skip root node)
4051  for(ti = articleTree->first()->next(); ti; ti = articleTree->next(ti))
4052  {
4053  ti->labelfont(FL_HELVETICA);
4054  }
4055  articleTree->redraw();
4056 
4057  // Mark all articles read
4058  // Note: CAC was already applied by group selection CB
4059  if(currentGroup->lwm && currentGroup->hwm >= currentGroup->lwm)
4060  {
4061  // Group is not empty
4062  for(a = currentGroup->hwm; a >= currentGroup->lwm; --a)
4063  {
4064  core_mark_as_read(&group_list[group_list_index], a);
4065  }
4066  }
4067 
4068  // Update group list
4069  groupListUpdateEntry(group_list_index);
4070  UI_STATUS(S("Marked all articles in group read."));
4071  }
4072 
4073  if(!stateMachine(EVENT_A_MAAR_EXIT))
4074  {
4075  PRINT_ERROR("Error in main window state machine");
4076  }
4077  }
4078 }
4079 
4080 
4081 // =============================================================================
4082 // Main Window mark (all articles in) all groups read callback
4083 
4084 void MainWindow::magar_cb_i(void)
4085 {
4086  Fl_Tree_Item* ti;
4087  std::size_t i;
4088  core_groupdesc* g;
4089  core_anum_t a;
4090  int rv;
4091 
4092  if(stateMachine(EVENT_A_MAGAR))
4093  {
4094  // Ask for confirmation
4095  fl_message_title(S("Warning"));
4096  rv = fl_choice("%s", S("No"),
4097  S("Yes"), NULL,
4098  S("Really mark all groups read?"));
4099  if(rv)
4100  {
4101  // Process all article tree nodes in current group (skip root node)
4102  if(NULL != currentGroup)
4103  {
4104  for(ti = articleTree->first()->next(); ti;
4105  ti = articleTree->next(ti))
4106  {
4107  ti->labelfont(FL_HELVETICA);
4108  }
4109  articleTree->redraw();
4110  }
4111 
4112  // Loop over all groups
4113  for(i = 0; group_num > i; ++i)
4114  {
4115  // Mark all articles read
4116  g = &subscribedGroups[i];
4117  if(g->lwm && g->hwm >= g->lwm)
4118  {
4119  // Group is not empty
4120  for(a = g->hwm; a >= g->lwm; --a)
4121  {
4122  core_mark_as_read(&group_list[i], a);
4123  }
4124  }
4125  // Update corresponding group list entry
4126  groupListUpdateEntry(i);
4127  }
4128  UI_STATUS(S("Marked all articles in all groups read."));
4129  }
4130 
4131  if(!stateMachine(EVENT_A_MAGAR_EXIT))
4132  {
4133  PRINT_ERROR("Error in main window state machine");
4134  }
4135  }
4136 }
4137 
4138 
4139 // =============================================================================
4140 // Main Window article tree callback
4141 
4142 void MainWindow::aselect_cb_i(void)
4143 {
4144  Fl_Tree_Item* ti = articleTree->callback_item();
4145 
4146  switch(articleTree->callback_reason())
4147  {
4148  case FL_TREE_REASON_OPENED:
4149  {
4150  // Check whether article was already read
4151  if(core_check_already_read(&group_list[group_list_index],
4152  (core_hierarchy_element*) ti->user_data()))
4153  {
4154  // Yes => Show selected article with normal font style
4155  ti->labelfont(FL_HELVETICA);
4156  }
4157  // Scroll tree so that opened item is on top
4158  scrollTree(UI_SCROLL_TOP, ti);
4159  break;
4160  }
4161  case FL_TREE_REASON_CLOSED:
4162  {
4163  // Check whether tree item has unread children
4164  if(checkTreeBranchForUnread(ti))
4165  {
4166  // Yes => Show selected article with bold font style
4167  ti->labelfont(FL_HELVETICA_BOLD);
4168  }
4169  break;
4170  }
4171  case FL_TREE_REASON_SELECTED:
4172  {
4173 #if 1
4174  // Workaround for unexpected callback with FL_TREE_REASON_SELECTED:
4175  // Sometimes FLTK create such callbacks even for deactivated entries
4176  if(NULL == ti->user_data())
4177  {
4178  PRINT_ERROR("No data associated with selected article item");
4179  break;
4180  }
4181 #endif
4182  // Check whether operation is allowed at the moment
4183  if(stateMachine(EVENT_A_PREPARE))
4184  {
4185  // Check whether tree item has unread children
4186  if(ti->is_open() || !checkTreeBranchForUnread(ti))
4187  {
4188  // Show selected article with normal font (marked read)
4189  // if there are no unread children or they are open/visible
4190  ti->labelfont(FL_HELVETICA);
4191  }
4192  // Store former article HE
4193  lastArticleHE = currentArticleHE;
4194  // Set current article hierarchy element to corresponding article
4195  currentArticleHE = (core_hierarchy_element*) ti->user_data();
4196  // Display corresponding article in content window (use own locking)
4197  articleSelect(UI_CB_START);
4198  // Scroll tree, so that selected item is in the middle
4199  scrollTree(UI_SCROLL_MIDDLE, ti);
4200  // Store selected item so that it can be restored
4201  // (this stored state is used later if the operation is not allowed)
4202  articleTree->store_current(ti);
4203  }
4204  else
4205  {
4206  // Restore former state if operation is not allowed
4207  articleTree->select_former();
4208  }
4209  break;
4210  }
4211  default:
4212  {
4213  break;
4214  }
4215  }
4216 }
4217 
4218 
4219 // =============================================================================
4220 // Main window strip 'angle-addr' token of 'From' header field
4221 //
4222 // The data is modified inside the buffer pointed to by 'from'.
4223 
4224 void MainWindow::stripAngleAddress(char* from)
4225 {
4226  char* p;
4227  char* q;
4228  std::size_t i;
4229  bool strip = false;
4230 
4231  // Attention: Overloaded and both prototypes different than in C!
4232  p = std::strrchr(from, (int) '<');
4233  // Attention: Overloaded and both prototypes different than in C!
4234  q = std::strrchr(from, (int) '>');
4235  if(NULL != p && NULL != q && q > p + 1)
4236  {
4237  // 'angle-addr' token present => Check for 'display-name'
4238  for(i = 0; i < (std::size_t) (p - from); ++i)
4239  {
4240  if(' ' != from[i]) { strip = true; break; }
4241  }
4242  if(strip)
4243  {
4244  // Strip 'angle-addr' and potential space before it
4245  if(' ' == *(p - 1)) { --p; }
4246  *p = 0;
4247  }
4248  else
4249  {
4250  // Extract 'addr-spec'
4251  *q = 0;
4252  std::memmove((void*) from, (void*) (p + 1), (std::size_t) (q - p));
4253  }
4254  }
4255 }
4256 
4257 
4258 // =============================================================================
4259 // Main window reply by e-mail callback
4260 
4261 void MainWindow::sendEmail(void)
4262 {
4263  const char* recipient;
4264  const char* subject;
4265  const char* body = NULL;
4266  int rv;
4267  int invalid = 0;
4268  int warn = 0;
4269  Fl_Text_Buffer tb;
4270  const char* p;
4271  char* q = NULL;
4272  char* r = NULL;
4273  char* s = NULL;
4274  char* t = NULL;
4275  std::size_t i = 0;
4276  char* sig_delim;
4277  char* sig = NULL;
4278  struct core_article_header* hdr = NULL;
4279  char* from = NULL;
4280  std::size_t len;
4281 
4282  if(NULL == currentGroup)
4283  {
4284  SC("Do not use non-ASCII for the translation of this item")
4285  fl_message_title(S("Error"));
4286  fl_alert("%s", S("No group selected"));
4287  }
4288  else if(NULL == currentArticleHE)
4289  {
4290  SC("Do not use non-ASCII for the translation of this item")
4291  fl_message_title(S("Error"));
4292  fl_alert("%s", S("No article selected"));
4293  }
4294  else
4295  {
4296  recipient = currentArticleHE->header->reply2;
4297  if(NULL == recipient) { recipient = currentArticleHE->header->from; }
4298  if(NULL == recipient)
4299  {
4300  // This should never happen because the core should provide strings for
4301  // all mandatory header fields.
4302  PRINT_ERROR("No e-mail address found (bug)");
4303  }
4304  else
4305  {
4306  // Check address of recipient
4307  recipient = enc_extract_addr_spec(recipient);
4308  if(NULL == recipient) { invalid = 1; }
4309  else
4310  {
4311  // Attention: Overloaded and both prototypes different than in C!
4312  p = std::strrchr(recipient, (int) '.');
4313  if(p)
4314  {
4315  // Check for reserved top level domains according to RFC 2606
4316  if(!std::strcmp(p, ".test")) { invalid = 1; }
4317  if(!std::strcmp(p, ".example")) { invalid = 1; }
4318  if(!std::strcmp(p, ".invalid")) { invalid = 1; }
4319  if(!std::strcmp(p, ".localhost")) { invalid = 1; }
4320  }
4321  if(!invalid)
4322  {
4323  // Cite article
4324  hdr = currentArticleHE->header;
4325  t = currentArticle->text();
4326  if(NULL != t)
4327  {
4328  // Strip header
4329  while('_' != t[i++]);
4330  while('|' != t[i++]);
4331  while('|' != t[i++]);
4332  q = &t[++i];
4333  // Strip signature
4334  r = q;
4335  while(1)
4336  {
4337  // Attention:
4338  // Overloaded and both prototypes different than in C!
4339  sig_delim = std::strstr(r, "\n-- \n");
4340  if(NULL == sig_delim) { break; }
4341  else { sig = r = &sig_delim[1]; }
4342  }
4343  if(NULL != sig) { sig[0] = 0; }
4344  // Extract authors 'display-name' and strip 'angle-addr'
4345  len = std::strlen(hdr->from);
4346  from = new char[++len];
4347  std::strcpy(from, hdr->from);
4348  stripAngleAddress(from);
4349  tb.insert(0, q);
4350  gui_cite_content(&tb, from, hdr->groups);
4351  body = tb.text();
4352  delete[] from;
4353  // Do not convert body to canonical form here!
4354  }
4355  // Create subject
4356  subject = currentArticleHE->header->subject;
4357  s = new char[std::strlen(subject) + (std::size_t) 5];
4358  std::strcpy(s, "Re: ");
4359  std::strcat(s, gui_check_re_prefix(subject));
4360  // Attention: Overloaded and both prototypes different than in C!
4361  q = std::strstr(s, "(was:");
4362  if(q && q != s)
4363  {
4364  // Attention:
4365  // Overloaded and both prototypes different than in C!
4366  if(std::strchr(q, (int) ')'))
4367  {
4368  *q = 0;
4369  if(' ' == *(--q)) { *q = 0; }
4370  }
4371  }
4372  subject = s;
4373  // Open warning popup if subject or body contains apostroph chars
4374  if(NULL != subject)
4375  {
4376  // Attention:
4377  // Overloaded and both prototypes different than in C!
4378  if(std::strchr(subject, 0x27)) { warn = 1; }
4379  }
4380  if(!warn && NULL != body)
4381  {
4382  // Attention:
4383  // Overloaded and both prototypes different than in C!
4384  if(std::strchr(body, 0x27)) { warn = 1; }
4385  }
4386  if(warn)
4387  {
4388  SC("Do not use non-ASCII for the translation of this item")
4389  fl_message_title(S("Note"));
4390  fl_message("%s",
4391  S("APOSTROPHE converted to\nRIGHT SINGLE QUOTATION MARK"));
4392  }
4393  // Start external e-mail handler
4394  rv = ext_handler_email(recipient, subject, body);
4395  if(rv)
4396  {
4397  SC("Do not use non-ASCII for the translation of this item")
4398  fl_message_title(S("Error"));
4399  fl_alert("%s", S("Starting e-mail client failed"));
4400  }
4401  delete[] s;
4402  std::free((void*) body);
4403  std::free((void*) t);
4404  }
4405  enc_free((void*) recipient);
4406  }
4407  if(invalid)
4408  {
4409  SC("Do not use non-ASCII for the translation of this item")
4410  fl_message_title(S("Error"));
4411  fl_alert("%s", S("Invalid e-mail address"));
4412  }
4413  }
4414  }
4415 }
4416 
4417 
4418 // =============================================================================
4419 // Main Window scroll down article content and skip to next article
4420 // (must be locked)
4421 
4422 void MainWindow::ascrolldown_cb(bool scroll)
4423 {
4424  int r; // Number of rows
4425  int f = -1; // First row
4426  int l = -1; // Last row
4427  int p = 0; // Position in text buffer
4428  int riv = 0; // Number of rows currently in view
4429  int scrollto;
4430  int x, y;
4431  int i;
4432  bool eoa = true;
4433  Fl_Tree_Item* fi;
4434  Fl_Tree_Item* ti;
4435  Fl_Tree_Item* oi;
4436  bool abort = false;
4437  bool wrap = false;
4438  bool found = false;
4439 
4440  // Check whether operation is allowed at the moment
4441  if(stateMachine(EVENT_SCROLL_NEXT))
4442  {
4443  // Scroll down if requested
4444  if(scroll)
4445  {
4446  // Calculate number of rows
4447  if(NULL != currentArticle && text->buffer() == currentArticle)
4448  {
4449  r = text->count_lines(0, text->buffer()->length(), true);
4450  if(0 < r)
4451  {
4452  for(i = 0; i < r; ++i)
4453  {
4454  if(text->position_to_xy(p, &x, &y))
4455  {
4456  if(0 > f) { l = f = i; }
4457  else { l = i; }
4458  }
4459  p = text->skip_lines(p, 1, true);
4460  }
4461  if(0 > f) { f = 0; l = 0; }
4462  currentLine = f;
4463  riv = l - f + 1;
4464  }
4465 #if 0
4466  // For debugging
4467  std::printf("-----------------------------\n");
4468  std::printf("Rows : %d\n", r);
4469  std::printf("Current row : %d\n", currentLine);
4470  std::printf("First in view: %d\n", f);
4471  std::printf("Last in view : %d\n", l);
4472  std::printf("Rows in view : %d\n", riv);
4473 #endif
4474 
4475  // Scroll down so that second last row will become first in view
4476  if(r && riv)
4477  {
4478  if(r - 1 != l)
4479  {
4480  scrollto = currentLine + riv - 2;
4481  if(scrollto < r - 1)
4482  {
4483  // std::printf("scrollto: %d\n", scrollto);
4484  text->scroll(scrollto + 1, 0);
4485  eoa = false;
4486  }
4487  }
4488  }
4489  }
4490  }
4491 
4492  // Skip to next unread article if at the end of current article
4493  if(eoa)
4494  {
4495  // Start with selected article (or first article if there is none)
4496  fi = articleTree->first_selected_item();
4497  if(NULL == fi)
4498  {
4499  fi = articleTree->first();
4500  if(fi == articleTree->root()) { fi = articleTree->next(fi); }
4501  }
4502  // Check for dummy article
4503  if(NULL != fi && NULL == fi->user_data()) { fi = NULL; }
4504  ti = fi;
4505  while(NULL != ti)
4506  {
4507  if(!core_check_already_read(&group_list[group_list_index],
4508  (core_hierarchy_element*) ti->user_data()))
4509  {
4510  // Open branch with next article
4511  oi = ti;
4512  while(articleTree->root() != oi)
4513  {
4514  if(!articleTree->open(oi)) { articleTree->open(oi, 1); }
4515  for(i = 0; i < articleTree->root()->children(); ++i)
4516  {
4517  if(articleTree->root()->child(i) == oi)
4518  {
4519  abort = true;
4520  break;
4521  }
4522  }
4523  if(abort) { break; }
4524  oi = articleTree->prev(oi);
4525  }
4526  // Select next article
4527  articleTree->deselect(fi, 0);
4528  articleTree->set_item_focus(ti);
4529  articleTree->select(ti, 1);
4530  found = true;
4531  break;
4532  }
4533  ti = articleTree->next(ti);
4534  if(NULL == ti && !wrap)
4535  {
4536  // Wrap to beginning once if no article was found yet
4537  wrap = true;
4538  ti = articleTree->first();
4539  if(ti == articleTree->root()) { ti = articleTree->next(ti); }
4540  }
4541  }
4542  }
4543  if(!stateMachine(EVENT_SCROLL_NEXT_EXIT))
4544  {
4545  PRINT_ERROR("Error in main window state machine");
4546  }
4547 
4548  // Sip to next group with unread articles (if no article was found)
4549  if (eoa && !found)
4550  {
4551  if (config[CONF_UNREAD_IN_NEXT_GROUP].val.i) { nug_cb_i(); }
4552  }
4553  }
4554 }
4555 
4556 
4557 // =============================================================================
4558 // Main Window server configuration update
4559 // (must be locked)
4560 
4561 void MainWindow::updateServer(int action)
4562 {
4563  SC("")
4564  SC("Do not use characters for the translation that cannot be converted to")
4565  SC("the ISO 8859-1 character set for this item.")
4566  SC("Leave the original string in place if in doubt.")
4567  const char* titleString = S("Server configuration");
4568  SC("")
4569  int rv = -1;
4570  int cancel = 0;
4571  ServerCfgWindow* scw = NULL;
4572  ServerConfig* sc = NULL;
4573  const char* user = NULL;
4574  const char* pass = NULL;
4575 
4576  // Start core to do the work
4577  if(UI_CB_START == action)
4578  {
4579  // Check whether operation is allowed at the moment
4580  if(!stateMachine(EVENT_SERVER)) { rv = 1; }
4581  else
4582  {
4583  // Display warning message
4584  data.data = NULL;
4585  SC("Do not use non-ASCII for the translation of this item")
4586  fl_message_title(S("Note"));
4587  SC("Line breaks are inserted with \n")
4588  fl_message("%s",
4589  S("Warning:\nGroup states are lost if the server is changed."));
4590 
4591  // Create object with current server configuration
4592  sc = new ServerConfig;
4593  sc->serverReplace(config[CONF_SERVER].val.s);
4594  sc->serviceReplace(config[CONF_SERVICE].val.s);
4595  sc->enc = config[CONF_ENC].val.i;
4596  sc->auth = config[CONF_AUTH].val.i;
4597 
4598  // Display "Server configuration" window
4599 #if CFG_USE_XSI && !CFG_NLS_DISABLE
4600  // Convert window title to the encoding required by the window manager
4601  // (WM). It is assumed that the WM can display either ISO 8859-1 or
4602  // UTF-8 encoded window titles.
4603  const char* title;
4604  title = gui_utf8_iso(titleString);
4605  if(NULL != title)
4606  {
4607  scw = new ServerCfgWindow(sc, title);
4608  delete[] title;
4609  }
4610 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
4611  scw = new ServerCfgWindow(sc, titleString);
4612 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
4613  if(NULL == scw)
4614  {
4615  // This should never happen
4616  PRINT_ERROR("Fatal error while creating server config window");
4617  exitRequest = 1;
4618  }
4619  else
4620  {
4621  if(0 < scw->process())
4622  {
4623  // Start operation
4624  UI_STATUS(S("Update server configuration ..."));
4625  UI_BUSY();
4626  core_mutex_lock();
4627  data.data = (void*) sc;
4629  // Disconnect so that changes take effect
4630  core_disconnect();
4631  // Export current groups (this creates groupfile if not present)
4632  rv = core_export_group_states(group_num, group_list);
4633  if(!rv)
4634  {
4635  // Check whether server name has changed
4636  if(std::strcmp(config[CONF_SERVER].val.s, sc->server))
4637  {
4638  // Reset states in groupfile and delete header database
4639  rv = core_reset_group_states(UI_CB_COOKIE_SERVER);
4640  }
4641  }
4642  }
4643  else { cancel = 1; }
4644  delete scw;
4645  }
4646  }
4647  }
4648  else
4649  {
4650  // Get result
4651  core_mutex_lock();
4652  rv = data.result;
4654  }
4655 
4656  // Check return value (positive value means "in progress")
4657  if(0 >= rv)
4658  {
4659  if(0 > rv)
4660  {
4661  if (!cancel)
4662  {
4663  // Operation failed
4664  UI_STATUS(S("Updating server configuration failed."));
4665  UI_READY();
4666  }
4667  }
4668  else
4669  {
4670  // Restore pointer to server string
4671  core_mutex_lock();
4672  sc = (ServerConfig*) data.data;
4674  // Replace server name in configuration
4675  // (This must be done after the header database was reset and ready for
4676  // the new server)
4677  conf_string_replace(&config[CONF_SERVER], sc->server);
4678  conf_string_replace(&config[CONF_SERVICE], sc->service);
4679  config[CONF_ENC].val.i = sc->enc;
4680  config[CONF_AUTH].val.i = sc->auth;
4681  if(UI_AUTH_USER == sc->auth)
4682  {
4683  // Ask for account name
4684  user = fl_input("%s", config[CONF_USER].val.s, "Login:");
4685  if(NULL != user) { conf_string_replace(&config[CONF_USER], user); }
4686  // Ask for password
4687  SC("Do not use non-ASCII for the translation of this item")
4688  fl_message_title(S("Note"));
4689  rv = fl_choice("%s", S("No"),
4690  S("Yes"), NULL,
4691  S("Store the password from the subsequent request?"));
4692  if(rv) { conf_ephemeral_passwd = 0; }
4693  else { conf_ephemeral_passwd = 1; }
4694  do
4695  {
4696  pass = fl_password("%s", config[CONF_PASS].val.s, "Password:");
4697  if(NULL != pass)
4698  {
4700  if(!config[CONF_PASS].val.s[0])
4701  {
4702  SC("Do not use non-ASCII for the translation of this item")
4703  fl_message_title(S("Warning"));
4704  fl_alert("%s", S("Empty password is not supported"));
4705  }
4706  }
4707  } while(NULL == pass || !config[CONF_PASS].val.s[0]);
4708  }
4709  // Reset CRL update interval back to epoch (force new CRLs)
4710  conf_string_replace(&config[CONF_CRL_UPD_TS], "1970-01-01T00:00:00Z");
4711  // Check for empty group list
4712  if(!group_num)
4713  {
4714  // Ask for password
4715  SC("Do not use non-ASCII for the translation of this item")
4716  fl_message_title(S("Note"));
4717  rv = fl_choice("%s", S("No"),
4718  S("Yes"), NULL,
4719  S("Ask server for group subscription proposals?"));
4720  if(rv)
4721  {
4722  // Ask server for subscription proposals
4723  groupListGetProposal(UI_CB_START);
4724  }
4725  else
4726  {
4727  UI_STATUS(S("No groups subscribed yet."));
4728  UI_READY();
4729  }
4730  }
4731  else
4732  {
4733  // Recreate group list
4734  core_destroy_subscribed_group_states(&group_num, &group_list);
4735  groupListRefresh(UI_CB_START);
4736  }
4737  }
4738  if(NULL != sc) { delete sc; }
4739  if(!stateMachine(EVENT_SERVER_EXIT))
4740  {
4741  PRINT_ERROR("Error in main window state machine");
4742  }
4743  }
4744 }
4745 
4746 
4747 // =============================================================================
4748 // Main window group subscription
4749 // (must be locked)
4750 
4751 void MainWindow::groupSubscribe(int action)
4752 {
4753  int rv = -1;
4754  std::size_t labelcount;
4755  core_grouplabel* labels;
4756  std::size_t i, ii, j;
4757  char* name;
4758  const char* label;
4759  int free_label;
4760  core_anum_t num;
4761  int fake_empty_response = 0;
4762 
4763  // Start core to do the work
4764  if(UI_CB_START == action)
4765  {
4766  if(!stateMachine(EVENT_SUBSCRIBE)) { rv = 1; }
4767  else
4768  {
4769  // Start operation
4770  UI_STATUS(S("Receiving group list ..."));
4771  UI_BUSY();
4772  rv = core_get_group_list(UI_CB_COOKIE_GROUPLIST);
4773  // Note: This will generate callback with action UI_CB_CONTINUE
4774  }
4775  }
4776  else
4777  {
4778  // Get result
4779  core_mutex_lock();
4780  rv = data.result;
4782  }
4783 
4784  if(!rv && UI_CB_CONTINUE == action)
4785  {
4786  core_mutex_lock();
4787  groupcount = data.size;
4788  grouplist = (core_groupdesc*) data.data;
4790  // Start next operation
4791  UI_STATUS(S("Receiving group labels ..."));
4792  UI_BUSY();
4793  rv = core_get_group_labels(UI_CB_COOKIE_GROUPLABELS);
4794  // Note: This will generate callback with action UI_CB_FINISH
4795  if(0 > rv) { fake_empty_response = 1; }
4796  }
4797 
4798  if(0 > rv && UI_CB_FINISH == action) { fake_empty_response = 1; }
4799 
4800  if (fake_empty_response)
4801  {
4802  // Fake valid empty response
4803  core_mutex_lock();
4804  data.size = 0;
4805  data.data = NULL;
4807  rv = 0;
4808  }
4809 
4810  // Check return value (positive value means "in progress")
4811  if(0 >= rv)
4812  {
4813  if(0 > rv)
4814  {
4815  // Operation failed
4816  UI_STATUS(S("Downloading group information failed."));
4817  UI_READY();
4818  }
4819  else
4820  {
4821  // Operation finished
4822  core_mutex_lock();
4823  labelcount = data.size;
4824  labels = (core_grouplabel*) data.data;
4826  UI_STATUS(S("Group information received."));
4827  // Create subscribe window
4828  SC("")
4829  SC("Do not use characters for the translation that cannot be")
4830  SC("converted to the ISO 8859-1 character set for this item.")
4831  SC("Leave the original string in place if in doubt.")
4832  subscribeWindow = new SubscribeWindow(S("Subscribe"),
4833  grouplist, labels);
4834  SC("")
4835  // Add groups
4836  UI_STATUS(S("Process group information ..."));
4837  subscribeWindow->hide();
4838  Fl::wait();
4839  for(i = 0; i < groupcount; ++i)
4840  {
4841  free_label = 0;
4842  // Do not remove this check until you know what you are doing!
4843  if(CORE_GROUP_FLAG_ASCII & grouplist[i].flags)
4844  {
4845  // Search for label
4846  name = grouplist[i].name;
4847  num = grouplist[i].eac;
4848  label = NULL;
4849  for(j = 0; j < labelcount; ++j)
4850  {
4851  if(!strcmp(labels[j].name, name))
4852  {
4853  // Group desciption label found
4854  // Verify UTF-8 encoding of label
4855  if(0 == enc_uc_check_utf8(labels[j].label))
4856  {
4857  // Valid
4858  label = labels[j].label;
4859  }
4860  else
4861  {
4862  // Not valid => Repair UTF-8 encoding
4863  PRINT_ERROR("Invalid encoding of group description");
4864  label = enc_uc_repair_utf8(labels[j].label);
4865  if(NULL != label) { free_label = 1; }
4866  }
4867  break;
4868  }
4869  }
4870  // Replace dots with slashes to create tree hierarchy
4871  ii = 0;
4872  do { if('.' == name[ii]) { name[ii] = '/'; } }
4873  while(name[ii++]);
4874  subscribeWindow->add(name, num, label);
4875  if(free_label) { core_free((void*) label); }
4876  }
4877  }
4878  // Collapse all branches of tree (and append labels, if present)
4879  // Must be the last operation after all groups are added
4880  subscribeWindow->collapseAll();
4881  subscribeWindow->show();
4882  UI_STATUS(S("Group information processed."));
4883  UI_READY();
4884  // The destructor of 'subscribeWindow' will release the memory for
4885  // 'grouplist' and 'labels'. Because this window is modal, no other
4886  // operations are possible in the meanwhile.
4887  }
4888  if(!stateMachine(EVENT_SUBSCRIBE_EXIT))
4889  {
4890  PRINT_ERROR("Error in main window state machine");
4891  }
4892  }
4893 }
4894 
4895 
4896 // =============================================================================
4897 // Main window calculate number of unread articles in group
4898 
4899 core_anum_t MainWindow::groupGetUnreadNo(core_anum_t lwm, core_anum_t hwm,
4900  struct core_range* info)
4901 {
4902  core_anum_t res = 0;
4903  core_anum_t i;
4904 
4905  if(lwm && hwm && hwm >= lwm)
4906  {
4907  res = hwm - lwm + (core_anum_t) 1;
4908  }
4909 
4910  if(res)
4911  {
4912  while(NULL != info)
4913  {
4914  for(i = info->last; i >= info->first; --i)
4915  {
4916  if(i >= lwm && i <= hwm) { if(res) { --res; } }
4917  }
4918  info = info->next;
4919  }
4920  }
4921 
4922  return(res);
4923 }
4924 
4925 
4926 // =============================================================================
4927 // Main window update entry i in group list
4928 
4929 void MainWindow::groupListUpdateEntry(std::size_t i)
4930 {
4931  std::ostringstream groupString;
4932  core_anum_t eac;
4933  core_anum_t ur;
4934  const char* fcs;
4935 
4936  // Show not more than 500 groups
4937  if((std::size_t) 500 >= i)
4938  {
4939  // Clamp article count to process
4940  groupCAC(&subscribedGroups[i]);
4941 
4942  // Calculate number of unread articles
4943  eac = subscribedGroups[i].eac;
4944  ur = groupGetUnreadNo(subscribedGroups[i].lwm, subscribedGroups[i].hwm,
4945  group_list[i].info);
4946  // Create entry for group list widget
4947  if(ur) { fcs = "@b"; } else { fcs = "@."; }
4948  groupString << fcs << group_list[i].name
4949  << " (" << ur << " / " << eac << ")" << std::flush;
4950 
4951  // Attention:
4952  //
4953  // const char* s = groupString.str().c_str()
4954  //
4955  // creates 's' with undefined value because the compiler is allowed to
4956  // free the temporary string object returned by 'str()' immediately after
4957  // the assignment!
4958  // Assigning names to temporary string objects forces them to stay in
4959  // memory as long as their names go out of scope (this is what we need).
4960  const std::string& gs = groupString.str();
4961 
4962  // Note: The index in the group list widget starts with 1
4963  if(++i > (std::size_t) groupList->size())
4964  {
4965  // Create new entry
4966  groupList->add(gs.c_str(), NULL);
4967  }
4968  else
4969  {
4970  // Replace label of existing entry
4971  groupList->text((int) i, gs.c_str());
4972  }
4973  }
4974 }
4975 
4976 
4977 // =============================================================================
4978 // Merge current group states into groupfile
4979 
4980 int MainWindow::groupStateMerge(void)
4981 {
4982  int res;
4983 
4984  // Skip this if group count is zero (or the groupfile will be cleared)
4985  if(group_num)
4986  {
4987  res = core_export_group_states(group_num, group_list);
4988  if(res)
4989  {
4990  SC("Do not use non-ASCII for the translation of this item")
4991  fl_message_title(S("Error"));
4992  fl_alert("%s", S("Exporting group states failed"));
4993  }
4994  }
4995  // Re-import current group states from groupfile
4996  res = core_update_subscribed_group_states(&group_num, &group_list,
4997  &group_list_index);
4998 
4999  return(res);
5000 }
5001 
5002 
5003 // =============================================================================
5004 // Main window ask server for proposed groups to subscribe
5005 // (must be locked)
5006 
5007 void MainWindow::groupListGetProposal(int action)
5008 {
5009  int rv = -1;
5010  char* grouplist_raw;
5011 
5012  // Start core to do the work
5013  if(UI_CB_START == action)
5014  {
5015  // Group list not empty
5016  if(!stateMachine(EVENT_GL_PROPOSAL)) { rv = 1; }
5017  else
5018  {
5019  // Get group states from server
5020  UI_STATUS(S("Get group subscription proposals ..."));
5021  UI_BUSY();
5022  rv = core_get_subscription_proposals(UI_CB_COOKIE_GROUPPROPOSAL);
5023  }
5024  }
5025  else
5026  {
5027  // Get result
5028  core_mutex_lock();
5029  rv = data.result;
5031  }
5032 
5033  // Check return value (positive value means "in progress")
5034  if(0 >= rv)
5035  {
5036  if(0 > rv)
5037  {
5038  // Operation failed
5039  UI_STATUS(S("Receiving proposed groups failed."));
5040  UI_READY();
5041  }
5042  else
5043  {
5044  core_mutex_lock();
5045  grouplist_raw = (char*) data.data;
5047  // Populate group list
5048  rv = gui_populate_group_list(grouplist_raw);
5049  if(rv)
5050  {
5051  PRINT_ERROR("Updating list of subscribed groups failed");
5052  }
5053  core_free(grouplist_raw);
5054  // Refresh group list
5055  groupListRefresh(UI_CB_START);
5056  }
5057  if(!stateMachine(EVENT_GL_PROPOSAL_EXIT))
5058  {
5059  PRINT_ERROR("Error in main window state machine");
5060  }
5061  }
5062 }
5063 
5064 
5065 // =============================================================================
5066 // Main window refresh group list with subscribed groups
5067 // (must be locked)
5068 
5069 void MainWindow::groupListRefresh(int action)
5070 {
5071  int rv = -1;
5072  std::size_t i;
5073  int sg = 0; // Selected group
5074  int g;
5075 
5076  // Start core to do the work
5077  if(UI_CB_START == action)
5078  {
5079  // Group list not empty
5080  if(!stateMachine(EVENT_GL_REFRESH)) { rv = 1; }
5081  else
5082  {
5083  rv = groupStateMerge();
5084  if(!rv)
5085  {
5086  // Get group states from server
5087  UI_STATUS(S("Refreshing subscribed groups ..."));
5088  UI_BUSY();
5089  for(g = 1; groupList->size() >= g; ++g)
5090  {
5091  if(groupList->selected(g)) { sg = g; break; }
5092  }
5093  groupRefresh_cb_state = sg;
5094  rv = core_get_subscribed_group_info(&group_num, &group_list,
5095  UI_CB_COOKIE_GROUPINFO1);
5096  // Note: This will generate callback with action UI_CB_CONTINUE
5097  }
5098  }
5099  }
5100  else
5101  {
5102  // Get result
5103  core_mutex_lock();
5104  rv = data.result;
5106  }
5107 
5108  if(!rv && UI_CB_CONTINUE == action)
5109  {
5110  // Continue operation
5111  if(NULL != subscribedGroups)
5112  {
5113  core_destroy_subscribed_group_info(&subscribedGroups);
5114  }
5115  core_mutex_lock();
5116  subscribedGroups = (core_groupdesc*) data.data;
5118  // Update group list widget
5119  groupList->clear();
5120  for(i = 0; i < group_num; ++i) { groupListUpdateEntry(i); }
5121  // Restore current group of server
5122  if(group_num && NULL != currentGroup)
5123  {
5124  rv = core_set_group(group_list[group_list_index].name,
5125  UI_CB_COOKIE_GROUPINFO2);
5126  // Note: This will generate callback with action UI_CB_FINISH
5127  }
5128  }
5129 
5130  if(!rv && UI_CB_FINISH == action)
5131  {
5132  // Update current group descriptor
5133  core_free(currentGroup);
5134  core_mutex_lock();
5135  currentGroup = (core_groupdesc*) data.data;
5137  }
5138 
5139  // Check return value (positive value means "in progress")
5140  if(0 >= rv)
5141  {
5142  if(0 > rv)
5143  {
5144  // Operation failed
5145  clearTree();
5146  core_free(currentGroup);
5147  currentGroup = NULL;
5148  UI_STATUS(S("Refreshing subscribed groups failed."));
5149  }
5150  else
5151  {
5152  groupList->deselect();
5153  if(!group_num || unsub)
5154  {
5155  // Delete article tree and current article
5156  clearTree();
5157  core_free(currentGroup);
5158  currentGroup = NULL;
5159  }
5160  else
5161  {
5162  if (0 == groupRefresh_cb_state)
5163  {
5164  if (0 != groupList->size())
5165  {
5166  groupList->select(1, 0);
5167  // FLTK 1.4.0 does not correctly draw the focus box
5168  groupList->redraw();
5169  }
5170  }
5171  else
5172  {
5173  // Reset group list widget
5174  groupList->select(groupRefresh_cb_state);
5175  groupList->middleline(groupRefresh_cb_state);
5176  }
5177  }
5178  // Operation finished
5179  UI_STATUS(S("Subscribed groups refreshed."));
5180  }
5181  unsub = false;
5182  UI_READY();
5183  if(!stateMachine(EVENT_GL_REFRESH_EXIT))
5184  {
5185  PRINT_ERROR("Error in main window state machine");
5186  }
5187  }
5188 }
5189 
5190 
5191 // =============================================================================
5192 // Main window group selection
5193 // (must be locked)
5194 
5195 void MainWindow::groupSelect(int action, int index)
5196 {
5197  int rv = -1;
5198 
5199  if(UI_CB_START == action)
5200  {
5201  if(!index || !stateMachine(EVENT_G_SELECT))
5202  {
5203  if(NULL != currentGroup)
5204  {
5205  // Reset group list window if operation is not allowed
5206  if((std::size_t) INT_MAX > group_list_index)
5207  {
5208  groupList->deselect();
5209  groupList->select((int) group_list_index + 1);
5210  }
5211  }
5212  rv = 1;
5213  }
5214  else
5215  {
5216  // Start core to do the work
5217  if(0 < index)
5218  {
5219  group_new = index;
5220  if(group_old)
5221  {
5222  group_old = 0;
5223  if((std::size_t) INT_MAX > group_list_index)
5224  {
5225  group_old = (int) group_list_index + 1;
5226  }
5227  }
5228  group_list_index = (std::size_t) index;
5229  if(--group_list_index < group_num)
5230  {
5231  UI_STATUS(S("Set new current group ..."));
5232  UI_BUSY();
5233  groupSelect_cb_state = groupList->value();
5234  rv = core_set_group(group_list[group_list_index].name,
5235  UI_CB_COOKIE_GROUP);
5236  }
5237  }
5238  }
5239  }
5240  else
5241  {
5242  // Get result
5243  core_mutex_lock();
5244  rv = data.result;
5246  }
5247 
5248  // Check return value (positive value means "in progress")
5249  if(0 >= rv)
5250  {
5251  UI_READY();
5252  if(0 > rv)
5253  {
5254  clearTree();
5255  UI_STATUS(S("Setting new current group failed."));
5256  // After error, the previous group is still selected
5257  if(0 >= group_old)
5258  {
5259  // No previous group
5260  groupList->select(group_new, 0);
5261  groupList->make_visible(group_new);
5262  }
5263  else
5264  {
5265  group_list_index = group_old - 1;
5266  groupList->deselect();
5267  groupList->select(group_old);
5268  groupList->make_visible(group_old);
5269  }
5270  if(stateMachine(EVENT_AT_REFRESH))
5271  {
5272  if(!stateMachine(EVENT_AT_REFRESH_EXIT))
5273  {
5274  PRINT_ERROR("Error in main window state machine");
5275  }
5276  }
5277  }
5278  else
5279  {
5280  // Success, update current group
5281  group_old = -1;
5282  core_free(currentGroup);
5283  core_mutex_lock();
5284  currentGroup = (core_groupdesc*) data.data;
5286  groupList->make_visible(group_new);
5287  // Clamp article count to process
5288  groupCAC(currentGroup);
5289  UI_STATUS(S("New current group set."));
5290 
5291  // Update article tree
5292  lastArticleHE = NULL;
5293  currentArticleHE = NULL;
5294  updateArticleTree(UI_CB_START);
5295  }
5296  if(!stateMachine(EVENT_G_SELECT_EXIT))
5297  {
5298  PRINT_ERROR("Error in main window state machine");
5299  }
5300  }
5301 }
5302 
5303 
5304 // =============================================================================
5305 // Main window clamp article count to configured value
5306 
5307 void MainWindow::groupCAC(core_groupdesc* g)
5308 {
5309  int i = config[CONF_CAC].val.i;
5310  core_anum_t diff;
5311 
5312  if(NULL != g && 0 < i--)
5313  {
5314  // Ensure that group is not empty
5315  if(g->hwm >= g->lwm && g->eac)
5316  {
5317  // Calculate new watermark difference
5318  diff = (core_anum_t) i;
5319  // Check whether clamping is required
5320  if(g->hwm - g->lwm > diff) { g->lwm = g->hwm - diff; }
5321  }
5322  }
5323 }
5324 
5325 
5326 // =============================================================================
5327 // Main window clear article tree (and current article)
5328 
5329 void MainWindow::clearTree(void)
5330 {
5331  Fl_Text_Buffer* tb;
5332  Fl_Tree_Item* ti;
5333 
5334  // Delete article tree
5335  articleTree->item_labelfont(FL_HELVETICA);
5336  articleTree->clear();
5337  ti = articleTree->add(S("No articles"));
5338  if(NULL != ti) { ti->deactivate(); }
5339  articleTree->redraw();
5340  // Hierarchy stays in memory until next group is selected
5341  // Delete last article
5342  lastArticleHE = NULL;
5343  // Delete current article
5344  currentArticleHE = NULL;
5345  // Preserve greeting message on startup
5346  if(startup) { startup = false; }
5347  else
5348  {
5349  tb = new Fl_Text_Buffer(0, 0);
5350  articleUpdate(tb);
5351  }
5352 }
5353 
5354 
5355 // =============================================================================
5356 // Main Window scroll article tree
5357 //
5358 // The original Fl_Tree widget of 1.3.0 requires the following algorithm for
5359 // scrolling to work correctly:
5360 // - Set vertical position to zero
5361 // - Redraw widget to update internal state
5362 // - Wait until redraw is complete
5363 // - Scroll to target position
5364 //
5365 // Since 1.3.3 the new Fl_Tree widget provides a 'calc_tree()' method.
5366 
5367 void MainWindow::scrollTree(ui_scroll position, Fl_Tree_Item* ti)
5368 {
5369  bool old_tree_widget = true;
5370 
5371  // Prepare internal state of widget
5372 #ifdef FL_ABI_VERSION
5373 # if 10303 <= FL_ABI_VERSION
5374  // Requires FLTK 1.3.3
5375  articleTree->hposition(0);
5376  articleTree->calc_tree();
5377  articleTree->hposition(0);
5378  old_tree_widget = false;
5379 # endif // 10303 <= FL_ABI_VERSION
5380 #endif // FL_ABI_VERSION
5381  if(old_tree_widget)
5382  {
5383  // Move vertical scrollbar to the top first
5384  articleTree->vposition(0);
5385  // Redraw tree so that new internal state is calculated for scrolling
5386  articleTree->redraw();
5387  articleTree->not_drawn();
5388  // Wait until redraw is finished
5389  while(!articleTree->drawn()) { Fl::wait(0.10); }
5390  // Wait until redraw is complete
5391  }
5392 
5393  // Scroll horizontally
5394 #ifdef FL_ABI_VERSION
5395 # if 10303 <= FL_ABI_VERSION
5396  // Requires FLTK 1.3.3
5397  gui_set_default_font();
5398  {
5399  int xmax = ti->draw_item_content(0);
5400  int scroll = xmax - (articleTree->x() + articleTree->w());
5401 
5402  // Check whether vertical scrollbar is visible
5403  if(articleTree->is_vscroll_visible())
5404  {
5405  // Yes => Add width of vertical scrollbar
5406  int w_sb = articleTree->scrollbar_size();
5407  if (0 == w_sb) { w_sb = Fl::scrollbar_size(); }
5408  scroll += w_sb;
5409  }
5410  if (0 < scroll)
5411  {
5412  // Check whether left side will be scrolled out of view
5413  int left = ti->x() - articleTree->x();
5414  if (left < scroll)
5415  {
5416  // Yes => Scroll to the left side
5417  scroll = left;
5418  }
5419  articleTree->hposition(scroll);
5420  }
5421  }
5422 # endif // 10303 <= FL_ABI_VERSION
5423 #endif // FL_ABI_VERSION
5424 
5425  // Scroll vertically
5426  switch(position)
5427  {
5428  case UI_SCROLL_TOP:
5429  {
5430  articleTree->show_item_top(ti);
5431  break;
5432  }
5433  case UI_SCROLL_MIDDLE:
5434  {
5435  articleTree->show_item_middle(ti);
5436  break;
5437  }
5438  case UI_SCROLL_BOTTOM:
5439  {
5440  articleTree->show_item_bottom(ti);
5441  break;
5442  }
5443  case UI_SCROLL_NONE:
5444  default:
5445  {
5446  // Ignore ti pointer
5447  break;
5448  }
5449  }
5450 
5451  articleTree->redraw();
5452 }
5453 
5454 
5455 // =============================================================================
5456 // Main Window check whether tree branch below item contains unread articles
5457 // This method must be reentrant.
5458 
5459 bool MainWindow::checkTreeBranchForUnread(Fl_Tree_Item* ti)
5460 {
5461  bool res = false;
5462  int c;
5463  int i;
5464 
5465  if(ti->children())
5466  {
5467  c = ti->children();
5468  for(i = 0; i < c; ++i)
5469  {
5470  // Check this child
5471  if(FL_HELVETICA_BOLD == ti->child(i)->labelfont()) { res = true; }
5472  else
5473  {
5474  // Recursively check children of this child
5475  if(checkTreeBranchForUnread(ti->child(i))) { res = true; }
5476  }
5477  if(true == res) { break; }
5478  }
5479  }
5480 
5481  return(res);
5482 }
5483 
5484 
5485 // =============================================================================
5486 // Main Window check whether tree branch below 'ti' contains item 'sti'
5487 // This method must be reentrant.
5488 
5489 bool MainWindow::checkTreeBranchForItem(Fl_Tree_Item* ti, Fl_Tree_Item* sti)
5490 {
5491  bool res = false;
5492  int c;
5493  int i;
5494 
5495  if(ti->children())
5496  {
5497  c = ti->children();
5498  for(i = 0; i < c; ++i)
5499  {
5500  // Check this child
5501  if(ti->child(i) == sti) { res = true; }
5502  else
5503  {
5504  // Recursively check children of this child
5505  if(checkTreeBranchForItem(ti->child(i), sti)) { res = true; }
5506  }
5507  if(true == res) { break; }
5508  }
5509  }
5510 
5511  return(res);
5512 }
5513 
5514 
5515 // =============================================================================
5516 // Main window add children to article tree node
5517 //
5518 // \param[in] cti Pointer to node of tree widget
5519 // \param[in] che Pointer to node of core article hierarchy
5520 //
5521 // Call this method with \e cti and \e che pointing to the root node of the
5522 // corresponding hierarchy. To add the child nodes, this method calls itself
5523 // recursively and must therefore be reentrant.
5524 //
5525 // This method first adds every node with bold font (marked unread).
5526 // Then the corresponding article is checked. If it's already read, the font is
5527 // changed to normal style (marked read).
5528 //
5529 // \note
5530 // This method is called for every article until CAC limit. It should be fast.
5531 //
5532 // \return
5533 // - The corresponding tree item if the current article was found
5534 // - \c NULL otherwise
5535 
5536 Fl_Tree_Item* MainWindow::addTreeNodes(Fl_Tree_Item* cti,
5538 {
5539  static const char t[] = " | "; // Field separator
5540  std::size_t t_len = sizeof(t);
5541  const char* s; // Subject
5542  std::size_t s_len;
5543  char s_tmp; // Temporary buffer used to convert HTAB to SP
5544  const char* f; // From
5545  std::size_t f_len;
5546  char* f_tmp; // Pointer to temporary buffer used for <angle-addr>
5547  core_time_t d_raw;
5548  char d[20]; // Date (Format: "YYYY-MM-DD HH:MM:SS")
5549  std::size_t d_len;
5550 #if USE_LINE_COUNT
5551  unsigned long int l_raw;
5552  char l[11]; // Lines
5553 #endif // USE_LINE_COUNT
5554 #if USE_ARTICLE_NUMBER
5555  char a[17]; // Article number
5556 #endif // USE_ARTICLE_NUMBER
5557  std::size_t l_len = 0;
5558  std::size_t a_len = 0;
5559  Fl_Tree_Item* res = NULL;
5560  Fl_Tree_Item* tmp = NULL;
5561  Fl_Tree_Item* nti = NULL;
5562  char* ss;
5563  std::size_t ssi;
5564  std::size_t i;
5565  std::size_t ii;
5566  int rv;
5567  int score;
5568  // For merging orphaned thread branches
5569  bool child_of_root_node = false;
5570  Fl_Tree_Item* rti;
5571  int iii;
5572  struct core_article_header* h;
5573  struct core_article_header* h2;
5574  // For unthreaded view
5575  core_time_t rd;
5576  core_anum_t n;
5577  core_anum_t rn;
5578  bool match;
5579  // Kill threshold
5580  int kill_threshold = conf_integer_check(CONF_SCORE_KILL_TH, -49999, 0);
5581 
5582  // Explicitly set foreground color for tree items to FLTK default
5583  // (default is not the same as for the other widgets)
5584  articleTree->item_labelfgcolor(FL_FOREGROUND_COLOR);
5585 
5586  // Set default font to bold (mark unread)
5587  articleTree->item_labelfont(FL_HELVETICA_BOLD);
5588 
5589  // Add children of current hierarchy element
5590  if(articleTree->root() == cti) { child_of_root_node = true; }
5591  for(i = 0; i < che->children; ++i)
5592  {
5593  n = che->child[i]->anum;
5594  // Create description line in format "Subject | From | Date[ | Lines]"
5595  // Prepare data and calculate length
5596  s = che->child[i]->header->subject;
5597  s_len = std::strlen(s);
5598  f = che->child[i]->header->from;
5599  f_len = t_len + std::strlen(f);
5600  d_raw = che->child[i]->header->date;
5601  rv = enc_convert_posix_to_iso8601(d, d_raw);
5602  if(rv) { d_len = 0; } else { d_len = t_len + (std::size_t) 20; }
5603 #if USE_LINE_COUNT
5604  l_raw = che->child[i]->header->lines;
5605  enc_convert_lines_to_string(l, l_raw);
5606  l_len = t_len + (std::size_t) 11;
5607 #else // USE_LINE_COUNT
5608  l_len = 0;
5609 #endif // USE_LINE_COUNT
5610 #if USE_ARTICLE_NUMBER
5611  rv = enc_convert_anum_to_ascii(a, &a_len, che->child[i]->anum);
5612  if(rv)
5613  {
5614  a[0] = 'E';
5615  a[1] = 'r';
5616  a[2] = 'r';
5617  a[3] = 'o';
5618  a[4] = 'r';
5619  a[5] = 0;
5620  a_len = 0;
5621  }
5622  else { a_len += t_len; }
5623 #else // USE_ARTICLE_NUMBER
5624  a_len = 0;
5625 #endif // USE_ARTICLE_NUMBER
5626  // Allocate buffer for description line
5627  ss = new char[s_len + f_len + d_len + l_len + a_len + (std::size_t) 1];
5628  ssi = 0; // Current index in 'ss'
5629  // -----------------------------------------------------------------------
5630  // Subject
5631  ii = 0; while(s[ii])
5632  {
5633  s_tmp = s[ii++];
5634  // The NNTP command OVER cannot transport HTAB characters (converts
5635  // them to SP according to RFC 3977). We always do this here to get
5636  // consistent results for cases where OVER is not used or the HTAB was
5637  // transported inside a MIME encoded-word.
5638  if (0x09 == (int) s_tmp) { ss[ssi++] = ' '; }
5639  else { ss[ssi++] = s_tmp; }
5640  }
5641  // -----------------------------------------------------------------------
5642  // From
5643  ii = 0; while(t[ii]) { ss[ssi++] = t[ii++]; }
5644  // Strip 'angle-addr' with standard method
5645  // (Note: Optimization was tested to be useless here)
5646  f_tmp = new char[f_len + (std::size_t) 1]; // For zero length separator
5647  std::strcpy(f_tmp, f);
5648  stripAngleAddress(f_tmp);
5649  ii = 0; while(f_tmp[ii]) { ss[ssi++] = f_tmp[ii++]; }
5650  delete[] f_tmp;
5651  // -----------------------------------------------------------------------
5652  // Date
5653  if(!rv)
5654  {
5655  ii = 0; while(t[ii]) { ss[ssi++] = t[ii++]; }
5656  ii = 0; while(d[ii]) { ss[ssi++] = d[ii++]; }
5657  }
5658  // -----------------------------------------------------------------------
5659  // Line count
5660 #if USE_LINE_COUNT
5661  ii = 0; while(t[ii]) { ss[ssi++] = t[ii++]; }
5662  ii = 0; while(l[ii]) { ss[ssi++] = l[ii++]; }
5663 #endif // USE_LINE_COUNT
5664  // -----------------------------------------------------------------------
5665  // Article number
5666 #if USE_ARTICLE_NUMBER
5667  ii = 0; while(t[ii]) { ss[ssi++] = t[ii++]; }
5668  ii = 0; while(a[ii]) { ss[ssi++] = a[ii++]; }
5669 #endif // USE_ARTICLE_NUMBER
5670  // -----------------------------------------------------------------------
5671  // Terminate description line string
5672  ss[ssi] = 0;
5673 
5674  // Add new node with the created description line
5675  if(config[CONF_TVIEW].val.i)
5676  {
5677  // --------------------------------------------------------------------
5678  // Insert new node (threaded view)
5679  if(child_of_root_node)
5680  {
5681  // Special handling for children of the root node
5682  rti = NULL;
5683  for(iii = 0; iii < cti->children(); ++iii)
5684  {
5685  // Try to merge orphaned thread branches by first reference
5686  // (all articles with this same anchor belong to the same thread)
5687  h = che->child[i]->header;
5688  if(NULL != h->refs)
5689  {
5690  h2 = ((core_hierarchy_element*) cti->child(iii)->user_data())
5691  ->header;
5692  if(NULL != h2->refs)
5693  {
5694  if(!strcmp(h2->refs[0], h->refs[0]))
5695  {
5696  rti = cti->child(iii);
5697  break;
5698  }
5699  }
5700  }
5701  }
5702  if(NULL == rti) { rti = cti; }
5703  nti = articleTree->add(rti, ss);
5704  }
5705  else { nti = articleTree->add(cti, ss); }
5706  // --------------------------------------------------------------------
5707  }
5708  else
5709  {
5710  // --------------------------------------------------------------------
5711  // Insert new node as child of root node (unthreaded view)
5712  for(rti = articleTree->next(articleTree->first());
5713  rti; rti = articleTree->next(rti))
5714  {
5715  match = false;
5716  if(config[CONF_UTVIEW_AN].val.i)
5717  {
5718  // Sort unthreaded view by article number
5719  rn = ((core_hierarchy_element*) rti->user_data())->anum;
5720  if(!config[CONF_INV_ORDER].val.i)
5721  {
5722  // Normal order (older above newer)
5723  if(rn >= n) { match = true; }
5724  }
5725  else
5726  {
5727  // Inversed order (older below newer)
5728  if(rn < n) { match = true; }
5729  }
5730  }
5731  else
5732  {
5733  // Sort unthreaded view by posting data
5734  rd = ((core_hierarchy_element*) rti->user_data())->header->date;
5735  if(!config[CONF_INV_ORDER].val.i)
5736  {
5737  // Normal order (older above newer)
5738  if(rd >= d_raw) { match = true; }
5739  }
5740  else
5741  {
5742  // Inversed order (older below newer)
5743  if(rd < d_raw) { match = true; }
5744  }
5745  }
5746  if(match)
5747  {
5748  nti = articleTree->insert_above(rti, ss);
5749  break;
5750  }
5751  }
5752  if(NULL == rti) { nti = articleTree->add(articleTree->root(), ss); }
5753  // --------------------------------------------------------------------
5754  }
5755  nti->user_data((void*) che->child[i]);
5756  delete[] ss;
5757 
5758  // -----------------------------------------------------------------------
5759  // Check whether node gets an icon
5760  // Priority order (low to high):
5761  // Positive score -> Reply to own -> Negative score -> Own
5762  score = filter_get_score(che->child[i]);
5763  // Check for positive score
5764  if(0 < score) { nti->usericon(&pm_score_up); }
5765  // Check for reply to own article
5766  if(filter_match_reply_to_own(che->child[i]))
5767  {
5768  nti->usericon(&pm_reply_to_own);
5769  }
5770  // Check for negative score
5771  if(0 > score)
5772  {
5773  nti->usericon(&pm_score_down);
5774  // Mark article read
5775  core_mark_as_read(&group_list[group_list_index], che->child[i]->anum);
5776  }
5777  // Check for own article
5778  if(filter_match_own(che->child[i])) { nti->usericon(&pm_own); }
5779  // -----------------------------------------------------------------------
5780 
5781  // Check whether article that corresponds to this node was already read
5782  if(core_check_already_read(&group_list[group_list_index], che->child[i]))
5783  {
5784  nti->labelfont(FL_HELVETICA);
5785  }
5786 
5787  // Kill articles with negative score below threshold
5788  if(kill_threshold > score)
5789  {
5790  if(main_debug)
5791  {
5792  printf("%s: %sKill article with score %d (threshold: %d)\n",
5793  CFG_NAME, MAIN_ERR_PREFIX, score, kill_threshold);
5794  }
5795  articleTree->remove(nti);
5796  nti = cti;
5797  }
5798 
5799  // Recursively add children of current node
5800  if(che->child[i]->children) { tmp = addTreeNodes(nti, che->child[i]); }
5801  // Check whether article was the last one read
5802  if(NULL == res)
5803  {
5804  if(NULL != tmp) { res = tmp; }
5805  else
5806  {
5807  if(group_list[group_list_index].last_viewed == che->child[i]->anum)
5808  {
5809  res = nti;
5810  }
5811  }
5812  }
5813  }
5814 
5815  // Update group list
5816  groupListUpdateEntry(group_list_index);
5817 
5818  return(res);
5819 }
5820 
5821 
5822 // =============================================================================
5823 // Main window update article tree widget
5824 
5825 void MainWindow::updateTree(void)
5826 {
5827  int rv;
5829  Fl_Tree_Item* rti; // Root tree item
5830  Fl_Tree_Item* child_of_root;
5831  Fl_Tree_Item* sti = NULL; // Stored (last viewed) tree item
5832  int i;
5833  bool unread_articles_present;
5834  Fl_Tree_Item* ti;
5835  Fl_Tree_Item* oi;
5836  bool recalculate = true;
5837  bool abort = false;
5838 
5839  articleTree->update_in_progress(true);
5840 
5841  // Redraw tree so that new internal state is recalculated
5842  articleTree->redraw();
5843  articleTree->not_drawn();
5844  // Wait until redraw is finished
5845  while(!articleTree->drawn()) { Fl::wait(0.10); }
5846 
5847  // Create a root item if there is none
5848  rti = articleTree->root();
5849  if(!rti)
5850  {
5851  articleTree->add("X");
5852  rti = articleTree->root();
5853  }
5854  // Delete all children of root item
5855  articleTree->clear_children(rti);
5856 
5857  // Redraw tree so that new internal state is recalculated
5858  articleTree->redraw();
5859  articleTree->not_drawn();
5860  // Wait until redraw is finished
5861  while(!articleTree->drawn()) { Fl::wait(0.10); }
5862 
5863  // Load overview lines from core article hierarchy into the tree widget
5864  rv = core_hierarchy_manager(NULL, CORE_HIERARCHY_GETROOT, 0, &che);
5865  if(!rv) { sti = addTreeNodes(rti, che); }
5866  // Collapse all branches of tree
5867  collapseTree();
5868 
5869  // Delete all branches that contain no unread articles on request
5870  if(config[CONF_ONLYUR].val.i)
5871  {
5872  while(recalculate)
5873  {
5874  recalculate = false;
5875  rv = rti->children();
5876  for(i = 0; i < rv; ++i)
5877  {
5878  if(FL_HELVETICA_BOLD == rti->child(i)->labelfont())
5879  {
5880  unread_articles_present = true;
5881  }
5882  else
5883  {
5884  unread_articles_present
5885  = checkTreeBranchForUnread(rti->child(i));
5886  }
5887  if(!unread_articles_present)
5888  {
5889  // Check whether last viewed article will be deleted
5890  if( rti->child(i) == sti
5891  || checkTreeBranchForItem(rti->child(i), sti) )
5892  {
5893  sti = NULL;
5894  }
5895  // This method deletes all children too
5896  articleTree->remove(rti->child(i));
5897  recalculate = true;
5898  break;
5899  }
5900  }
5901  }
5902  // Check for empty tree
5903  if(!rti->children())
5904  {
5905  ti = articleTree->add(S("No articles"));
5906  if(NULL != ti)
5907  {
5908  ti->deactivate();
5909  ti->labelfont(FL_HELVETICA);
5910  ti->user_data(NULL);
5911  }
5912  }
5913  }
5914 
5915  // Show the toplevel item of each branch with bold font to indicate that it
5916  // contains unread articles
5917  rv = rti->children();
5918  for(i = 0; i < rv; ++i)
5919  {
5920  unread_articles_present = checkTreeBranchForUnread(rti->child(i));
5921  if(unread_articles_present)
5922  {
5923  rti->child(i)->labelfont(FL_HELVETICA_BOLD);
5924  }
5925  }
5926 
5927  // Check whether there is a last viewed article stored
5928  if(NULL != sti)
5929  {
5930  // Select last viewed article
5931  ti = rti;
5932  // Assignment in truth expression is intended
5933  while(NULL != (ti = articleTree->next(ti)))
5934  {
5935  if(sti == ti)
5936  {
5937  // Open branch with last viewed article
5938  oi = ti;
5939  while(articleTree->root() != oi)
5940  {
5941  if(!articleTree->open(oi)) { articleTree->open(oi, 1); }
5942  for(i = 0; i < articleTree->root()->children(); ++i)
5943  {
5944  if(articleTree->root()->child(i) == oi)
5945  {
5946  abort = true;
5947  break;
5948  }
5949  }
5950  if(abort) { break; }
5951  oi = articleTree->prev(oi);
5952  }
5953  // Select last viewed article
5954  articleTree->set_item_focus(ti);
5955  articleTree->select(ti, 1);
5956  Fl::focus(articleTree);
5957  break;
5958  }
5959  }
5960  }
5961  else
5962  {
5963  // Scroll down to last (or first) branch
5964  if(rti->children())
5965  {
5966  if(!config[CONF_INV_ORDER].val.i)
5967  {
5968  // Last branch
5969  child_of_root = rti->child(rti->children() - 1);
5970  }
5971  else
5972  {
5973  // First branch (inverted order)
5974  child_of_root = rti->child(0);
5975  }
5976  scrollTree(UI_SCROLL_BOTTOM, child_of_root);
5977  articleTree->set_item_focus(child_of_root);
5978  Fl::focus(articleTree);
5979  }
5980  // Clear article content window
5981  currentArticle->text("");
5982  currentStyle->text("");
5983  }
5984  articleTree->redraw();
5985 
5986  articleTree->update_in_progress(false);
5987 }
5988 
5989 
5990 // =============================================================================
5991 // Main window create article hierarchy
5992 // (must be locked)
5993 
5994 void MainWindow::updateArticleTree(int action)
5995 {
5996  int rv = -1; // Negative: Error, 0: Success, 1: In progress, 2: Empty
5997  const char* header;
5998 
5999  if(UI_CB_START == action)
6000  {
6001  if(!stateMachine(EVENT_AT_REFRESH)) { rv = 1; }
6002  else
6003  {
6004  state = 0;
6005  // Push horizontal scrollbar left
6006 #ifdef FL_ABI_VERSION
6007 # if 10303 <= FL_ABI_VERSION
6008  // Requires FLTK 1.3.3
6009  articleTree->hposition(0);
6010 # endif // 10303 <= FL_ABI_VERSION
6011 #endif // FL_ABI_VERSION
6012  // Init article hierarchy
6014  if(!rv)
6015  {
6016  // Check whether group is empty
6017  if(!currentGroup->eac) { rv = 2; }
6018  else
6019  {
6020  // No => Fetch 1st header
6021  ai = currentGroup->lwm;
6022  UI_STATUS(S("Update article tree ..."));
6023  UI_BUSY();
6024  // Try to fetch header overview
6025  ai_range.first = currentGroup->lwm;
6026  ai_range.last = currentGroup->hwm;
6027  ai_range.next = NULL;
6028  rv = core_get_overview(&ai_range, UI_CB_COOKIE_OVERVIEW);
6029  if(0 > rv)
6030  {
6031  // No header overview available => Fetch 1st header
6032  rv = core_get_article_header(&ai, UI_CB_COOKIE_HEADER);
6033  }
6034  }
6035  }
6036  }
6037  }
6038  else
6039  {
6040  if(-1 != state)
6041  {
6042  // Get result
6043  core_mutex_lock();
6044  rv = data.result;
6046  }
6047  }
6048 
6049  // Check return value
6050  if(0 >= rv || 2 == rv)
6051  {
6052  if(0 > rv)
6053  {
6054  // Failed
6055  clearTree();
6056  state = -1;
6057  }
6058  else if(2 == rv)
6059  {
6060  // Empty
6061  clearTree();
6062  state = 2;
6063  }
6064  else
6065  {
6066  // Success
6067  core_mutex_lock();
6068  header = (const char*) data.data;
6070  // Check whether overview is available
6071  if(UI_CB_FINISH == action)
6072  {
6073  core_create_hierarchy_from_overview(&group_list[group_list_index],
6074  &ai_range, header);
6075  core_free((void*) header);
6076  // Finished
6077  state = 1;
6078  }
6079  else
6080  {
6081  UI_PROGRESS((std::size_t) ai - currentGroup->lwm,
6082  (std::size_t) currentGroup->hwm - currentGroup->lwm);
6083  // Check for last article
6084  if(currentGroup->hwm == ai++)
6085  {
6086  // Finished
6087  state = 1;
6088  }
6089  else
6090  {
6091  // Fetch next header in parallel using core thread
6092  rv = core_get_article_header(&ai, UI_CB_COOKIE_HEADER);
6093  if(0 > rv) { state = -1; }
6094  }
6095  // Check whether article was canceled
6096  if(NULL == header)
6097  {
6098  // Yes => Mark read to achieve correct number of unread articles
6099  core_mark_as_read(&group_list[group_list_index], ai - 1U);
6100  }
6101  else
6102  {
6103  // No => Insert article into hierarchy
6105  ai - 1U, header);
6106  if(0 > rv)
6107  {
6108  PRINT_ERROR("Adding article to hierarchy failed");
6109  // Note: Don't change state here and continue
6110  }
6111  core_free((void*) header);
6112  }
6113  }
6114  }
6115  // Note: 'state' stay zero if there are more articles to fetch
6116  if(state)
6117  {
6118  // Reset group list widget
6119  groupList->deselect();
6120  groupList->select(groupSelect_cb_state);
6121  // Unlock before updating article tree widget
6122  if(!stateMachine(EVENT_AT_REFRESH_EXIT))
6123  {
6124  PRINT_ERROR("Error in main window state machine");
6125  }
6126  switch(state)
6127  {
6128  case 2: // Empty
6129  {
6130  UI_STATUS(S("Group is empty"));
6131  break;
6132  }
6133  case 1: // Finished
6134  {
6135  // Update UI widgets with data from core
6136  UI_BUSY();
6137  Fl::check();
6138  updateTree();
6139  UI_READY();
6140  UI_STATUS(S("Article tree updated."));
6141  break;
6142  }
6143  case -1: // Aborted
6144  {
6145  UI_READY();
6146  UI_STATUS(S("Updating article tree failed."));
6147  break;
6148  }
6149  default: // Bug
6150  {
6151  PRINT_ERROR("Invalid state while updating article tree");
6152  UI_READY();
6153  UI_STATUS(S("Updating article tree failed."));
6154  break;
6155  }
6156  }
6157  }
6158  }
6159 }
6160 
6161 
6162 // =============================================================================
6163 // Main window article update
6164 
6165 void MainWindow::articleUpdate(Fl_Text_Buffer* article)
6166 {
6167  static bool init = true;
6168  // See RFC 3986 for URI format
6169  const char* url[] = { "http://", "https://", "ftp://", "nntp://",
6170  "file://", "news:", "mailto:", NULL };
6171  const std::size_t url_len[] = { 7, 8, 6, 7, 7, 5, 7 };
6172  const char bold = 'A'; // Bold text for header field names
6173  const char sig = 'B'; // Signature starting with "-- " separator
6174  const char cit = 'C'; // External citation ("|" at start of line)
6175  const char link = 'D'; // Hyperlink
6176  const char plain = 'E'; // Normal article text
6177  const char l1 = 'F'; // 1st citation level
6178  const char l2 = 'G'; // 2nd citation level
6179  const char l3 = 'H'; // 3rd citation level
6180  const char l4 = 'I'; // 4th citation level
6181  Fl_Text_Buffer* ca = currentArticle;
6182  char* style;
6183  std::size_t len;
6184  std::size_t i;
6185  std::size_t ii = 0; // Index in current line
6186  std::size_t iii = 0;
6187  std::size_t iiii;
6188  std::size_t url_i;
6189  bool sol = true; // Start Of Line flag
6190  char hs = plain;
6191  bool references = false; // Flag indicating reference list in header
6192  bool ready = false; // Flag indicating positions beyond header separator
6193  int ss = 0;
6194  bool delim = false; // Hyperlink delimiter
6195  bool signature = false; // Flag indicating signature
6196  bool citation = false; // Flag indicating external citation
6197  bool hyperlink = false; // Flag indicating hyperlink
6198  std::size_t cl = 0;
6199  bool cl_lock = false;
6200  char c;
6201  int pos = 0;
6202 
6203  // Store hyperlink style
6204  hyperlinkStyle = (unsigned int) (unsigned char) link;
6205 
6206  // Replace current article
6207  if(NULL == article)
6208  {
6209  PRINT_ERROR("Article update request without content ignored (bug)");
6210  return;
6211  }
6212  gui_check_article(article);
6213  currentArticle = article;
6214  text->buffer(currentArticle);
6215  currentLine = 0;
6216  if(ca) { delete ca; }
6217 
6218  // Soft Hyphen (SHY) handling
6219  gui_process_shy(currentArticle);
6220 
6221  // Create article content style
6222  if(currentStyle) { delete currentStyle; }
6223  style = currentArticle->text();
6224  if(NULL == style) { len = 0; }
6225  else { len = std::strlen(style); }
6226  if(INT_MAX < len) { len = INT_MAX; }
6227  for(i = 0; i < len; ++i)
6228  {
6229  if('\n' == style[i])
6230  {
6231  sol = true;
6232  references = false;
6233  hyperlink = false;
6234  citation = false;
6235  continue;
6236  }
6237  if(hyperlink)
6238  {
6239  // Check for end of hyperlink
6240  // According to RFC 3986 whitespace, double quotes and angle brackets
6241  // are accepted as delimiters.
6242  // Whitespace is interpreted as SP or HT, Unicode whitespace is not
6243  // accepted as delimiter.
6244  c = style[i];
6245  if(' ' == c || 0x09 == (int) c || '>' == c || '"' == c)
6246  {
6247  hyperlink = false;
6248  }
6249  }
6250  // Highlight external citations (via '|' or '!')
6251  if(sol && ('|' == style[i] || '!' == style[i]))
6252  {
6253  if(!hyperlink) { citation = true; }
6254  }
6255  // Check for start of line
6256  if(sol) { sol = false; delim = true; ss = 0; cl = 0; ii = 0; }
6257  else { ++ii; }
6258  if(signature)
6259  {
6260  // Check for end of signature in potential multipart message
6261  if('|' == style[i])
6262  {
6263  if(79U == ii)
6264  {
6265  for(iiii = i - ii; iiii < i; ++iiii)
6266  {
6267  if('_' != currentArticle->byte_at((int) iiii)) { break; }
6268  }
6269  if(iiii == i)
6270  {
6271  for(iiii = 0; iiii <= ii; ++iiii)
6272  {
6273  style[i - iiii] = plain;
6274  }
6275  signature = false;
6276  }
6277  }
6278  }
6279  }
6280  if(!ready)
6281  {
6282  // Check for header separator <SOL>"____...____|"
6283  if(!ii)
6284  {
6285  if('_' != style[i]) { hs = bold; } else { hs = plain; }
6286  iii = 0;
6287  }
6288  if('|' == style[i] && 79U <= iii) { ready = true; }
6289  else if('_' == style[i]) { ++iii; }
6290  if(':' == style[i]) { hs = plain; }
6291  // Check for GS control character (reference link list marker)
6292  if(0x1D == (int) style[i]) { references = true; }
6293  if(references)
6294  {
6295  // Create hyperlinks to articles in references list
6296  if(0x30 <= style[i] && 0x39 >= style[i]) { hs = link; }
6297  else { hs = plain; }
6298  }
6299  style[i] = hs;
6300  }
6301  else
6302  {
6303  // Check for signature separator <SOL>"-- "
6304  if('-' == style[i] && !ii) { ss = 1; }
6305  if('-' == style[i] && 1U == ii && 1 == ss) { ss = 2; }
6306  if(' ' == style[i] && 2U == ii && 2 == ss)
6307  {
6308  // Attention: This EOL check requires POSIX line format!
6309  if((char) 0x0A == style[i + 1U])
6310  {
6311  if(gui_last_sig_separator(&style[i + 1U]))
6312  {
6313  style[i] = sig; style[i - 1U] = sig; style[i - 2U] = sig;
6314  signature = true;
6315  }
6316  }
6317  }
6318  // Check for hyperlink
6319  url_i = 0;
6320  while(NULL != url[url_i])
6321  {
6322  if(!std::strncmp(&style[i], url[url_i], url_len[url_i]))
6323  {
6324  if(delim)
6325  {
6326  style[i] = link;
6327  hyperlink = true;
6328  }
6329  }
6330  ++url_i;
6331  }
6332  c = style[i];
6333  if(' ' == c || 0x09 == (int) c || '<' == c || '"' == c)
6334  {
6335  delim = true;
6336  }
6337  else { delim = false; }
6338  if(1)
6339  {
6340  // Highlight citation levels of regular content
6341  if(!ii)
6342  {
6343  if('>' == style[i]) { cl_lock = false; }
6344  else { cl_lock = true; }
6345  }
6346  if('>' == style[i] && !cl_lock) { ++cl; }
6347  if('>' != style[i] && ' ' != style[i] && (char) 9 != style[i])
6348  {
6349  cl_lock = true;
6350  }
6351  if(4U < cl) { cl = 1; } // Rotate colors if too many levels
6352  switch(cl)
6353  {
6354  case 1: { style[i] = l1; break; }
6355  case 2: { style[i] = l2; break; }
6356  case 3: { style[i] = l3; break; }
6357  case 4: { style[i] = l4; break; }
6358  default: { style[i] = plain; break; }
6359  }
6360  }
6361  // Override current style for signature, citations and hyperlinks
6362  // (hyperlinks have highest precedence)
6363  if(signature) { style[i] = sig; }
6364  if(citation && !signature) { style[i] = cit; }
6365  if(hyperlink) { style[i] = link; }
6366  }
6367  }
6368  currentStyle = new Fl_Text_Buffer((int) len, 0);
6369  if(NULL != style) { currentStyle->text(style); }
6370  std::free((void*) style);
6371 
6372  // Special handling for greeting message (always display as plain text)
6373  if(init) { init = false; }
6374  else
6375  {
6376  // Activate content style
6377  text->highlight_data(currentStyle, styles, styles_len, 'A', NULL, NULL);
6378  }
6379 
6380  // Replace GS marker with SP
6381  if (1 == currentArticle->findchar_forward(0, 0x1DU, &pos))
6382  {
6383  currentArticle->replace(pos, pos + 1, " ");
6384  }
6385 
6386  // Scroll to top/left position
6387  text->scroll(0, 0);
6388 }
6389 
6390 
6391 // =============================================================================
6392 // Main window format and print some header fields of article
6393 
6394 const char* MainWindow::printHeaderFields(struct core_article_header* h)
6395 {
6396  return(gui_print_header_fields(h));
6397 }
6398 
6399 
6400 // =============================================================================
6401 // Main window article selection
6402 // (must be locked)
6403 
6404 void MainWindow::articleSelect(int action)
6405 {
6406  int rv = -1;
6407  char* raw;
6408  char* eoh;
6409  const char* p = NULL;
6410  const char* q = NULL;
6411  Fl_Text_Buffer* tb;
6412  const char* hdr;
6413  bool overview = false;
6414  bool process = false;
6415 
6416  // Check whether current HE is available
6417  if(NULL == currentArticleHE)
6418  {
6419  // Print error message and terminate gracefully
6420  SC("Do not use non-ASCII for the translation of this item")
6421  fl_message_title(S("Error"));
6422  fl_alert("%s", "Fatal error detected in 'articleSelect()' (bug)");
6423  exitRequest = 1;
6424  return;
6425  }
6426 
6427  // Check whether current HE contains only overview data
6428  if(CORE_HE_FLAG_OVER & currentArticleHE->flags) { overview = true; };
6429 
6430  if(UI_CB_START == action)
6431  {
6432  // Check whether operation is allowed at the moment
6433  if(!stateMachine(EVENT_A_SELECT)) { rv = 1; }
6434  else if(currentArticleHE)
6435  {
6436  // Start core to fetch article body
6437  if(main_debug)
6438  {
6439  printf("%s: %sArticle selected\n", CFG_NAME, MAIN_ERR_PREFIX);
6440  }
6441  UI_STATUS(S("Download article ..."));
6442  UI_BUSY();
6443  startup = false;
6444  if(overview)
6445  {
6446  // Fetch whole article (post-fetch complete header data)
6447  rv = core_get_article(&currentArticleHE->anum, UI_CB_COOKIE_BODY);
6448  }
6449  else
6450  {
6451  // Fetch only body, header data in HE are already complete
6452  rv = core_get_article_body(&currentArticleHE->anum,
6453  UI_CB_COOKIE_BODY);
6454  }
6455  }
6456  }
6457  else
6458  {
6459  // Get result
6460  core_mutex_lock();
6461  rv = data.result;
6463  }
6464 
6465  // Check return value (positive value means "in progress")
6466  if(0 >= rv)
6467  {
6468  UI_READY();
6469  if(!rv)
6470  {
6471  process = true;
6472  core_mutex_lock();
6473  raw = (char*) data.data;
6475  p = raw;
6476  // Reset wrap mode
6477  wrapMode = Fl_Text_Display::WRAP_NONE;
6478  text->wrap_mode(wrapMode, 0);
6479  // Extract header data and update HE if required
6480  if(!overview) { q = p; }
6481  else
6482  {
6483  // Search for end of header
6484  // Attention: Overloaded and both prototypes different than in C!
6485  eoh = std::strstr(raw, "\r\n\r\n");
6486  if(NULL == eoh)
6487  {
6488  core_free((void*) p);
6489  process = false;
6490  }
6491  else
6492  {
6493  // Set pointer to body data
6494  q = &eoh[4];
6495  // Update HE
6496  eoh[2] = 0;
6498  currentArticleHE->anum, p);
6499  if(0 > rv) { process = false; }
6500  }
6501  }
6502  }
6503  if(!process)
6504  {
6505  // Failed
6506  UI_STATUS(S("Download of article failed."));
6507  PRINT_ERROR("Processing of article failed");
6508  // Clear article content window
6509  currentArticle->text("");
6510  currentStyle->text("");
6511  }
6512  else
6513  {
6514  // Success, update article window
6515  UI_STATUS(S("Article successfully downloaded."));
6516  // Create text buffer
6517  tb = new Fl_Text_Buffer(0, 0);
6518 #if 0
6519  // For debugging
6520  // The 'printf()' implementation must be able to handle NULL pointers!
6521  std::printf("\nArticle watermark ............: %lu\n",
6522  currentArticleHE->anum);
6523  std::printf("MIME version .................: %s\n",
6524  currentArticleHE->header->mime_v);
6525  std::printf("MIME content transfer encoding: %s\n",
6526  currentArticleHE->header->mime_cte);
6527  std::printf("MIME content type ............: %s\n",
6528  currentArticleHE->header->mime_ct);
6529 #endif
6530  // Print formatted header data to text buffer
6531  hdr = printHeaderFields(currentArticleHE->header);
6532  if(NULL != hdr)
6533  {
6534  tb->text(hdr);
6535  delete[] hdr;
6536  }
6537  // Print header/body delimiter
6538  tb->append(ENC_DELIMITER);
6539  // Create MIME object from content
6540  if(NULL != mimeData) { delete mimeData; }
6541  mimeData = new MIMEContent(currentArticleHE->header, q);
6542  core_free((void*) p); p = NULL;
6543  gui_decode_mime_entities(tb, mimeData,
6544  currentArticleHE->header->msgid);
6545  // Print content
6546  articleUpdate(tb);
6547  // Store last viewed article
6548  group_list[group_list_index].last_viewed = currentArticleHE->anum;
6549  }
6550  // Mark current article read
6551  core_mark_as_read(&group_list[group_list_index],
6552  currentArticleHE->anum);
6553  groupListUpdateEntry(group_list_index);
6554  // Check for error
6555  if(!process) { currentArticleHE = NULL; }
6556  else
6557  {
6558  // Reset search start position
6559  currentSearchPosition = 0;
6560  }
6561  if(!stateMachine(EVENT_A_SELECT_EXIT))
6562  {
6563  PRINT_ERROR("Error in main window state machine");
6564  }
6565  }
6566 }
6567 
6568 
6569 // =============================================================================
6570 // Main window view message of the day callback
6571 // (must be locked)
6572 
6573 void MainWindow::viewMotd(int action)
6574 {
6575  int rv = -1;
6576  std::ostringstream titleString;
6577  const char* motd = NULL;
6578  const char* p;
6579 
6580  if(UI_CB_START == action)
6581  {
6582  if(!stateMachine(EVENT_MOTD_VIEW)) { rv = 1; }
6583  else
6584  {
6585  // Start core to do the work
6586  UI_STATUS(S("Get message of the day ..."));
6587  UI_BUSY();
6588  rv = core_get_motd(UI_CB_COOKIE_MOTD);
6589  }
6590  }
6591  else
6592  {
6593  // Get result
6594  core_mutex_lock();
6595  rv = data.result;
6597  }
6598 
6599  // Check return value (positive value means "in progress")
6600  if(0 >= rv)
6601  {
6602  UI_READY();
6603  if(0 > rv) { UI_STATUS(S("No message of the day or download failed.")); }
6604  else
6605  {
6606  // Success
6607  core_mutex_lock();
6608  p = (const char*) data.data;
6610  UI_STATUS(S("Message of the day successfully downloaded."));
6611 
6612  // Verify UTF-8 encoding and convert to POSIX form
6613  if(enc_uc_check_utf8(p))
6614  {
6615  motd = core_convert_canonical_to_posix("[Invalid encoding]\r\n",
6616  1, 0);
6617  }
6618  else { motd = core_convert_canonical_to_posix(p, 1, 0); }
6619 
6620  // Create Unicode title for window
6621  SC("")
6622  SC("Do not use characters for the translation that cannot be")
6623  SC("converted to the ISO 8859-1 character set for this item.")
6624  SC("Leave the original string in place if in doubt.")
6625  titleString << S("Message of the day") << std::flush;
6626  SC("")
6627 
6628  // Attention:
6629  //
6630  // const char* title = titleString.str().c_str()
6631  //
6632  // creates 'title' with undefined value because the compiler is allowed
6633  // to free the temporary string object returned by 'str()' immediately
6634  // after the assignment!
6635  // Assigning names to temporary string objects forces them to stay in
6636  // memory as long as their names go out of scope (this is what we
6637  // need).
6638  const std::string& ts = titleString.str();
6639 
6640  // Set "Article source code" window title
6641 #if CFG_USE_XSI && !CFG_NLS_DISABLE
6642  // Convert window title to the encoding required by the window manager
6643  // (WM)
6644  // It is assumed that the WM can display either ISO 8859-1 or UTF-8
6645  // encoded window titles.
6646  const char* title;
6647  title = gui_utf8_iso(ts.c_str());
6648  if(NULL != title)
6649  {
6650  // Display "Message of the day" window
6651  new MotdWindow(motd, title);
6652  // Release memory for window title
6653  delete[] title;
6654  }
6655 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
6656  // Display "Message of the day" window
6657  new MotdWindow(motd, ts.c_str());
6658 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
6659  // Release memory for raw article content
6660  enc_free((void*) motd);
6661  core_free((void*) p);
6662  }
6663  if(!stateMachine(EVENT_MOTD_VIEW_EXIT))
6664  {
6665  PRINT_ERROR("Error in main window state machine");
6666  }
6667  }
6668 }
6669 
6670 
6671 // =============================================================================
6672 // Main window view article callback
6673 // (must be locked)
6674 //
6675 // Note: The parameter mid must be specified without angle brackets!
6676 
6677 void MainWindow::viewArticle(int action, const char* mid)
6678 {
6679  static const char debug_prefix[] = "Try to fetch article: ";
6680  int rv = -1;
6681  std::ostringstream titleString;
6682  const char* article;
6683  char* sbuf;
6684  std::size_t len;
6685 
6686  if(UI_CB_START == action)
6687  {
6688  if(!stateMachine(EVENT_A_VIEW)) { rv = 1; }
6689  else if(NULL != mid)
6690  {
6691  // Start core to do the work
6692  UI_STATUS(S("Get article ..."));
6693  UI_BUSY();
6694  // Add angle brackets
6695  mid_a = new char[std::strlen(mid) + (std::size_t) 3];
6696  mid_a[0] = '<';
6697  std::strcpy(&mid_a[1], mid);
6698  std::strcat(mid_a, ">");
6699  if(main_debug)
6700  {
6701  len = std::strlen(MAIN_ERR_PREFIX);
6702  len += std::strlen(debug_prefix);
6703  len += std::strlen(mid_a);
6704  sbuf = new char[len + (std::size_t) 1];
6705  std::strcpy(sbuf, MAIN_ERR_PREFIX);
6706  std::strcat(sbuf, debug_prefix);
6707  std::strcat(sbuf, mid_a);
6708  print_error(sbuf);
6709  delete[] sbuf;
6710  }
6711  rv = core_get_article_by_mid(mid_a, UI_CB_COOKIE_ARTICLE);
6712  }
6713  }
6714  else
6715  {
6716  // Get result
6717  core_mutex_lock();
6718  rv = data.result;
6720  // Free memory for Message-ID with angle brackets
6721  delete[] mid_a;
6722  }
6723 
6724  // Check return value (positive value means "in progress")
6725  if(0 >= rv)
6726  {
6727  UI_READY();
6728  if(0 > rv)
6729  {
6730  UI_STATUS(S("Downloading article failed."));
6731  SC("Do not use non-ASCII for the translation of this item")
6732  fl_message_title(S("Error"));
6733  fl_alert("%s", S("Article not found"));
6734  }
6735  else
6736  {
6737  // Success
6738  core_mutex_lock();
6739  article = (const char*) data.data;
6741  UI_STATUS(S("Article successfully downloaded."));
6742 
6743  // Create Unicode title for article window
6744  SC("")
6745  SC("Do not use characters for the translation that cannot be")
6746  SC("converted to the ISO 8859-1 character set for this item.")
6747  SC("Leave the original string in place if in doubt.")
6748  titleString << S("Article") << std::flush;
6749  SC("")
6750 
6751  // Attention:
6752  //
6753  // const char* title = titleString.str().c_str()
6754  //
6755  // creates 'title' with undefined value because the compiler is allowed
6756  // to free the temporary string object returned by 'str()' immediately
6757  // after the assignment!
6758  // Assigning names to temporary string objects forces them to stay in
6759  // memory as long as their names go out of scope (this is what we
6760  // need).
6761  const std::string& ts = titleString.str();
6762 
6763  // Set "Article" window title
6764 #if CFG_USE_XSI && !CFG_NLS_DISABLE
6765  // Convert window title to the encoding required by the window manager
6766  // (WM)
6767  // It is assumed that the WM can display either ISO 8859-1 or UTF-8
6768  // encoded window titles.
6769  const char* title;
6770  title = gui_utf8_iso(ts.c_str());
6771  if(NULL != title)
6772  {
6773  // Display "Article" window
6774  new ArticleWindow(article, title);
6775  // Release memory for window title
6776  delete[] title;
6777  }
6778 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
6779  // Display "Article" window
6780  new ArticleWindow(article, ts.c_str());
6781 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
6782  // Release memory for raw article content
6783  core_free((void*) article);
6784  }
6785  if(!stateMachine(EVENT_A_VIEW_EXIT))
6786  {
6787  PRINT_ERROR("Error in main window state machine");
6788  }
6789  }
6790 }
6791 
6792 
6793 // =============================================================================
6794 // Main window view article source code callback
6795 // (must be locked)
6796 
6797 void MainWindow::viewSrc(int action)
6798 {
6799  int rv = -1;
6800  std::ostringstream titleString;
6801  const char* article;
6802 
6803  if(UI_CB_START == action)
6804  {
6805  if(!stateMachine(EVENT_SRC_VIEW)) { rv = 1; }
6806  else if(NULL != currentArticleHE)
6807  {
6808  // Start core to do the work
6809  UI_STATUS(S("Get article source code ..."));
6810  UI_BUSY();
6811  rv = core_get_article(&currentArticleHE->anum, UI_CB_COOKIE_SRC);
6812  }
6813  }
6814  else
6815  {
6816  // Get result
6817  core_mutex_lock();
6818  rv = data.result;
6820  }
6821 
6822  // Check return value (positive value means "in progress")
6823  if(0 >= rv)
6824  {
6825  UI_READY();
6826  if(0 > rv) { UI_STATUS(S("Downloading article source code failed.")); }
6827  else
6828  {
6829  // Success
6830  core_mutex_lock();
6831  article = (const char*) data.data;
6833  UI_STATUS(S("Article source code successfully downloaded."));
6834 
6835  // Create Unicode title for article source code window
6836  SC("")
6837  SC("Do not use characters for the translation that cannot be")
6838  SC("converted to the ISO 8859-1 character set for this item.")
6839  SC("Leave the original string in place if in doubt.")
6840  titleString << S("Article source code") << std::flush;
6841  SC("")
6842 
6843  // Attention:
6844  //
6845  // const char* title = titleString.str().c_str()
6846  //
6847  // creates 'title' with undefined value because the compiler is allowed
6848  // to free the temporary string object returned by 'str()' immediately
6849  // after the assignment!
6850  // Assigning names to temporary string objects forces them to stay in
6851  // memory as long as their names go out of scope (this is what we
6852  // need).
6853  const std::string& ts = titleString.str();
6854 
6855  // Set "Article source code" window title
6856 #if CFG_USE_XSI && !CFG_NLS_DISABLE
6857  // Convert window title to the encoding required by the window manager
6858  // (WM)
6859  // It is assumed that the WM can display either ISO 8859-1 or UTF-8
6860  // encoded window titles.
6861  const char* title;
6862  title = gui_utf8_iso(ts.c_str());
6863  if(NULL != title)
6864  {
6865  // Display "Article source code" window
6866  new ArticleSrcWindow(article, title);
6867  // Release memory for window title
6868  delete[] title;
6869  }
6870 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
6871  // Display "Article source code" window
6872  new ArticleSrcWindow(article, ts.c_str());
6873 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
6874  // Release memory for raw article content
6875  core_free((void*) article);
6876  }
6877  if(!stateMachine(EVENT_SRC_VIEW_EXIT))
6878  {
6879  PRINT_ERROR("Error in main window state machine");
6880  }
6881  }
6882 }
6883 
6884 
6885 // =============================================================================
6886 // Main window article compose callback
6887 // (must be locked)
6888 //
6889 // reply / super Action
6890 // ----------------------------------------------
6891 // false / false Create a new message/thread
6892 // false / true Create a cancel control message
6893 // true / false Create a followup message
6894 // true / true Create a superseding message
6895 
6896 void MainWindow::articleCompose(bool reply, bool super)
6897 {
6898  const char* fqdn = config[CONF_FQDN].val.s;
6899  std::ostringstream titleString;
6900  char* p = NULL;
6901  char* q = NULL;
6902  const char* cq = NULL;
6903  std::size_t i = 0;
6904  char* sig_delim;
6905  char* sig = NULL;
6906  struct core_article_header* hdr = NULL;
6907  char* from = NULL;
6908  std::size_t len;
6909  Fl_Text_Buffer header;
6910  const char* msgid = NULL;
6911  const char* ckey1 = NULL;
6912  const char* ckey = NULL;
6913  const char* subject;
6914  const char* datetime;
6915  bool headerOK = true;
6916  int rv;
6917  int start;
6918  int end;
6919  unsigned int c = (unsigned int) '\n';
6920  int pos;
6921  const char* field_name;
6922  const char* field;
6923  const char* field2;
6924  bool insertedMessageID = false;
6925  int inspos;
6926  bool abort = true;
6927  bool error = true;
6928 
6929  // Allow only one composer at a time
6930  if(!stateMachine(EVENT_COMPOSE)) { return; }
6931 
6932  if(NULL == currentGroup)
6933  {
6934  SC("Do not use non-ASCII for the translation of this item")
6935  fl_message_title(S("Error"));
6936  fl_alert("%s", S("No group selected"));
6937  }
6938  else if((reply || super) && NULL == currentArticleHE)
6939  {
6940  SC("Do not use non-ASCII for the translation of this item")
6941  fl_message_title(S("Error"));
6942  fl_alert("%s", S("No article selected"));
6943  }
6944  else
6945  {
6946  if(reply || super) { hdr = currentArticleHE->header; }
6947  abort = false;
6948  }
6949  if(!abort)
6950  {
6951  // Check for special exception Fup2 poster
6952  if(reply && !super && NULL != hdr->fup2)
6953  {
6954  if(!std::strcmp("poster", hdr->fup2))
6955  {
6956  SC("Do not use non-ASCII for the translation of this item")
6957  fl_message_title(S("Warning"));
6958  rv = fl_choice("%s", S("Cancel"),
6959  S("OK"),
6960  S("Ignore"),
6961  S("Really execute Followup-To to poster?"));
6962  if(1 == rv) { sendEmail(); } // OK
6963  if(!rv || 1 == rv) { abort = true; }
6964  }
6965  }
6966  }
6967  if(!abort)
6968  {
6969  if(reply)
6970  {
6971  // Prepare current article content for citation
6972  if(currentArticle->selected())
6973  {
6974  // Some text was selected, use only this part
6975  p = currentArticle->selection_text();
6976  if(NULL != p) { cq = p; }
6977  }
6978  else
6979  {
6980  // Use complete article body
6981  p = currentArticle->text();
6982  if(NULL != p)
6983  {
6984  // Strip header
6985  while('_' != p[i++]);
6986  while('|' != p[i++]);
6987  while('|' != p[i++]);
6988  q = &p[++i];
6989  cq = q;
6990  // Strip signature
6991  while(1)
6992  {
6993  // Attention: Overloaded and both prototypes different than in C!
6994  sig_delim = std::strstr(q, "\n-- \n");
6995  if(NULL == sig_delim) { break; }
6996  else { sig = q = &sig_delim[1]; }
6997  }
6998  if(NULL != sig) { sig[0] = 0; }
6999  }
7000  }
7001  // Extract author name
7002  len = std::strlen(hdr->from);
7003  from = new char[++len];
7004  std::strcpy(from, hdr->from);
7005  stripAngleAddress(from);
7006  }
7007  else
7008  {
7009  if(super)
7010  {
7011  // Prepare default cancel reason text
7012  cq = "Reason for cancel unknown.\n";
7013  }
7014  else if(std::strlen(config[CONF_INITIAL_GREETING].val.s))
7015  {
7016  // Prepare initial greeting (if configured)
7018  }
7019  }
7020 
7021  // Create message header
7022  //
7023  // According to RFC 5536 the following rules are applied:
7024  // - Arbitrary ordering of header fields is allowed => We use common one
7025  // - Header field name must be followed by a colon and a space => We do so
7026  //
7027  // According to RFC 5537 the following rules are applied:
7028  // - Mandatory fields for proto article: From, Newsgroups, Subject
7029  // => We create at least these header fields
7030  // - The header field References must be no longer than 998 octets
7031  // after unfolding => We trim the references if this is the case
7032  header.append("Path: not-for-mail\n");
7033  if(std::strlen(fqdn))
7034  {
7035  header.append("Message-ID: ");
7036  msgid = core_get_msgid(fqdn);
7037  if(NULL == msgid) { headerOK = false; }
7038  else
7039  {
7040  header.append(msgid);
7041  insertedMessageID = true;
7042  }
7043  // Memory is released later because of Cancel-Lock!
7044  header.append("\n");
7045  }
7046  if(!std::strlen(config[CONF_FROM].val.s))
7047  {
7048  SC("Do not use non-ASCII for the translation of this item")
7049  fl_message_title(S("Error"));
7050  fl_alert("%s", S("From: header field not configured"));
7051  headerOK = false;
7052  }
7053  else
7054  {
7055  field_name = "From: ";
7056  field = enc_create_name_addr(config[CONF_FROM].val.s,
7057  std::strlen(field_name));
7058  if(NULL == field) { headerOK = false; }
7059  else
7060  {
7061  header.append(field_name);
7062  header.append(field);
7063  header.append("\n");
7064  // Only allow supersede or cancel for own messages
7065  if(super)
7066  {
7067  field2 = enc_create_name_addr(hdr->from,
7068  std::strlen(field_name));
7069  if(NULL == field2) { headerOK = false; }
7070  else
7071  {
7072  if(std::strcmp(field, field2))
7073  {
7074  SC("Do not use non-ASCII for the translation of this item")
7075  fl_message_title(S("Error"));
7076  fl_alert("%s", S("Supersede or cancel not allowed"));
7077  headerOK = false;
7078  }
7079  enc_free((void*) field2);
7080  }
7081  }
7082  enc_free((void*) field);
7083  }
7084  }
7085  header.append("Newsgroups: ");
7086  if(!super && !reply) { header.append(currentGroup->name); }
7087  else
7088  {
7089  len = 12;
7090  // Check for Fup2 header field
7091  rv = 0;
7092  if(!super && NULL != hdr->fup2)
7093  {
7094  if(std::strcmp("poster", hdr->fup2))
7095  {
7096  SC("Do not use non-ASCII for the translation of this item")
7097  fl_message_title(S("Note"));
7098  fl_message("%s", S("Followup-To specified groups executed"));
7099  header.append(hdr->fup2);
7100  len += std::strlen(hdr->fup2);
7101  rv = 1;
7102  }
7103  }
7104  if(!rv)
7105  {
7106  // Not found => Use current group list
7107  i = 0;
7108  do
7109  {
7110  if(i) { header.append(","); }
7111  ++len;
7112  header.append(hdr->groups[i]);
7113  len += std::strlen(hdr->groups[i]);
7114  }
7115  while(NULL != hdr->groups[++i]);
7116  }
7117  // Verify length but never fold "Newsgroups" header field
7118  if((std::size_t) 998 < len) { headerOK = false; }
7119  }
7120  header.append("\n");
7121  header.append("Subject: ");
7122  if(reply)
7123  {
7124  if(super) { subject = hdr->subject; }
7125  else
7126  {
7127  header.append("Re: ");
7128  subject = gui_check_re_prefix(hdr->subject);
7129  }
7130  header.append(subject);
7131  }
7132  else if(super)
7133  {
7134  // Old behaviour until 0.14
7135  //header.append(hdr->subject);
7136  // New behaviour since 0.15 (backward compatible to RFC 1036 syntax)
7137  if(NULL == hdr->msgid) { headerOK = false; }
7138  else
7139  {
7140  header.append("cmsg cancel ");
7141  header.append(hdr->msgid);
7142  }
7143  }
7144  header.append("\n");
7145  if(!super)
7146  {
7147  // Strip substring starting with "(was:" matched case insensitive
7148  rv = header.search_forward(0, "Subject:", &start, 1);
7149  if(1 == rv)
7150  {
7151  // Name match found => Verify that it starts at BOL
7152  if(0 < start) { c = header.char_at(start - 1); }
7153  if((unsigned int) '\n' == c)
7154  {
7155  // Yes => Search for EOL
7156  rv = header.findchar_forward(start, (unsigned int) '\n', &end);
7157  if(1 == rv)
7158  {
7159  rv = header.search_forward(start, "(was:", &pos, 0);
7160  if(1 == rv)
7161  {
7162  header.remove(pos, end);
7163  if(pos)
7164  {
7165  if((unsigned int) ' ' == header.char_at(pos - 1))
7166  {
7167  header.remove(pos - 1, pos);
7168  }
7169  }
7170  }
7171  }
7172  }
7173  }
7174  }
7175  header.append("Date: ");
7176  datetime = core_get_datetime(0);
7177  if(NULL == datetime) { headerOK = false; }
7178  else { header.append(datetime); }
7179  header.append("\n");
7180  // --- Optional header fields ---
7181  if(insertedMessageID)
7182  {
7183  // Create Cancel-Lock header if we have created the Message-ID
7186  if(NULL != ckey1 || NULL != ckey)
7187  {
7188  header.append("Cancel-Lock: ");
7189  if(NULL != ckey1) { header.append(ckey1); }
7190  if(NULL != ckey1 && NULL != ckey) { header.append(" " ); }
7191  if(NULL != ckey) { header.append(ckey); }
7192  header.append("\n");
7193  }
7194  core_free((void*) ckey);
7195  core_free((void*) ckey1);
7196  // RFC 5536 requires that this header is inserted at injection.
7197  // According to RFC 5537 an injecting agent is not allowed to add this
7198  // header field to a proto-article that already contains "Message-ID"
7199  // and "Date" header-fields. Therefore, if we have added these two
7200  // header-fields, we add "Injection-Date" too.
7201  header.append("Injection-Date: ");
7202  header.append(datetime);
7203  header.append("\n");
7204  }
7205  core_free((void*) datetime);
7206  core_free((void*) msgid); // Released here because of Cancel-Lock
7207  if(super)
7208  {
7209  msgid = hdr->msgid;
7210  if(NULL == msgid) { headerOK = false; }
7211  else
7212  {
7213  if(reply)
7214  {
7215  header.append("Supersedes: ");
7216  header.append(msgid);
7217  header.append("\n");
7218  }
7219  else
7220  {
7221  header.append("Control: cancel ");
7222  header.append(msgid);
7223  header.append("\n");
7224  }
7225  if(insertedMessageID)
7226  {
7227  // Create Cancel-Key header if we have created the Message-ID
7230  if(NULL != ckey1 || NULL != ckey)
7231  {
7232  header.append("Cancel-Key: ");
7233  if(NULL != ckey1) { header.append(ckey1); }
7234  if(NULL != ckey1 && NULL != ckey) { header.append(" " ); }
7235  if(NULL != ckey) { header.append(ckey); }
7236  header.append("\n");
7237  }
7238  core_free((void*) ckey1);
7239  core_free((void*) ckey);
7240  }
7241  }
7242  }
7243  if(std::strlen(config[CONF_REPLYTO].val.s))
7244  {
7245  field_name = "Reply-To: ";
7246  field = enc_create_name_addr(config[CONF_REPLYTO].val.s,
7247  std::strlen(field_name));
7248  if(NULL == field) { headerOK = false; }
7249  else
7250  {
7251  header.append(field_name);
7252  header.append(field);
7253  header.append("\n");
7254  enc_free((void*) field);
7255  }
7256  }
7257  // According to RFC 5537 the maximum length of the References header
7258  // field must be trimmed to 998 octets. Because Message-IDs are not
7259  // allowed to contain MIME encoded words, folding of this header
7260  // field is never required.
7261  if(reply)
7262  {
7263  len = 0;
7264  inspos = 0;
7265  // Add new reference for parent if not superseding
7266  if(!(NULL == hdr->refs && super))
7267  {
7268  header.append("References: ");
7269  inspos = header.length();
7270  len = 12;
7271  header.append("\n");
7272  }
7273  if(!super)
7274  {
7275  header.insert(inspos, hdr->msgid);
7276  len += std::strlen(hdr->msgid);
7277  }
7278  if(NULL != hdr->refs)
7279  {
7280  if(NULL != hdr->refs[0])
7281  {
7282  // Count first previous reference
7283  ++len;
7284  len += std::strlen(hdr->refs[0]);
7285  // Insert previous references (backward, beginning with last one)
7286  i = 0;
7287  while(NULL != hdr->refs[i]) { ++i; }
7288  while(--i)
7289  {
7290  len += std::strlen(hdr->refs[i]);
7291  if(UI_HDR_BUFSIZE < ++len) { break; }
7292  else
7293  {
7294  header.insert(inspos, " ");
7295  header.insert(inspos, hdr->refs[i]);
7296  }
7297  }
7298  // Always insert first previous reference
7299  header.insert(inspos, " ");
7300  header.insert(inspos, hdr->refs[0]);
7301  }
7302  }
7303  }
7304  if(std::strlen(config[CONF_ORGANIZATION].val.s))
7305  {
7306  header.append("Organization: ");
7307  header.append(config[CONF_ORGANIZATION].val.s);
7308  header.append("\n");
7309  }
7310  if(config[CONF_ENABLE_UAGENT].val.i)
7311  {
7312  header.append("User-Agent: " CFG_NAME "/" CFG_VERSION
7313  " (for " CFG_OS ")\n");
7314  }
7315  // Insert MIME header field
7316  // Note: It is mandatory for a MIME conformant client to send this header
7317  // field with all messages!
7318  header.append("MIME-Version: 1.0\n");
7319  // Insert header/body separator (empty line)
7320  header.append("\n");
7321 
7322  // Ensure that header was successfully created
7323  if(true != headerOK)
7324  {
7325  SC("Do not use non-ASCII for the translation of this item")
7326  fl_message_title(S("Error"));
7327  fl_alert("%s", S("Creating message header failed"));
7328  }
7329  else
7330  {
7331  const char* header_text;
7332  header_text = header.text();
7333  if(NULL != header_text)
7334  {
7335  // Create Unicode title for compose window
7336  SC("")
7337  SC("Do not use characters for the translation that cannot be")
7338  SC("converted to the ISO 8859-1 character set for the items")
7339  SC("in this section.")
7340  SC("Leave the original strings in place if in doubt.")
7341  if(reply)
7342  {
7343  titleString << S("Compose followup or reply") << std::flush;
7344  }
7345  else { titleString << S("Compose new article") << std::flush; }
7346  SC("")
7347  // Attention:
7348  //
7349  // const char* title = titleString.str().c_str()
7350  //
7351  // creates 'title' with undefined value because the compiler is allowed
7352  // to free the temporary string object returned by 'str()' immediately
7353  // after the assignment!
7354  // Assigning names to temporary string objects forces them to stay in
7355  // memory as long as their names go out of scope (this is what we
7356  // need).
7357  const std::string& ts = titleString.str();
7358 
7359  // Construct compose window
7360 #if CFG_USE_XSI && !CFG_NLS_DISABLE
7361  // Convert window title to the encoding required by the window manager
7362  // (WM)
7363  // It is assumed that the WM can display either ISO 8859-1 or UTF-8
7364  // encoded window titles.
7365  const char* title;
7366  title = gui_utf8_iso(ts.c_str());
7367  if(NULL != title)
7368  {
7369  // Construct article composer
7370  composeWindow = new ComposeWindow(title, header_text, cq, from,
7371  hdr, super);
7372  error = false;
7373  // Release memory for window title
7374  delete[] title;
7375  }
7376 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
7377  // Construct article composer
7378  composeWindow = new ComposeWindow(ts.c_str(), header_text, cq, from,
7379  hdr, super);
7380  error = false;
7381 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
7382  }
7383  std::free((void*) header_text);
7384  }
7385  }
7386 
7387  // Release memory
7388  if(NULL != from) { delete[] from; }
7389  std::free((void*) p);
7390 
7391  // Verify that compose windows was constructed
7392  if(error)
7393  {
7394  if(!stateMachine(EVENT_COMPOSE_EXIT))
7395  {
7396  PRINT_ERROR("Error in main window state machine");
7397  }
7398  }
7399 }
7400 
7401 
7402 // =============================================================================
7403 // Main window article posting callback
7404 // (must be locked)
7405 //
7406 // The pointer \e article must point to a memory block allocated by \c malloc()
7407 // and this function will call \c free(article) .
7408 
7409 void MainWindow::articlePost(int action, const char* article)
7410 {
7411  int rv = -1;
7412  const char* p;
7413  enum fm
7414  {
7415  FM_DEFAULT,
7416  FM_CORE,
7417  FM_EXT
7418  }
7419  free_method;
7420 
7421  free_method = FM_DEFAULT;
7422  if(UI_CB_START == action)
7423  {
7424  if(!stateMachine(EVENT_POST)) { rv = 1; }
7425  else
7426  {
7427  UI_STATUS(S("Post article ..."));
7428  UI_BUSY();
7429  // Convert article content line breaks from POSIX form to canonical
7430  // (RFC 822) form
7431  p = core_convert_posix_to_canonical(article);
7432  if(NULL == p)
7433  {
7434  SC("Do not use non-ASCII for the translation of this item")
7435  fl_message_title(S("Error"));
7436  fl_alert("%s", S("Conversion from local to canonical form failed"));
7437  }
7438  else
7439  {
7440  // Success
7441  if(p != article)
7442  {
7443  std::free((void*) article);
7444  article = p;
7445  free_method = FM_CORE;
7446  }
7447  // Call external postprocessor (while blocking UI)
7448  // The body of the article always has Unicode format
7449  p = ext_pp_filter(article);
7450  if(NULL == p)
7451  {
7452  SC("Do not use non-ASCII for the translation of this item")
7453  fl_message_title(S("Error"));
7454  fl_alert("%s", S("External postprocessor failed"));
7455  }
7456  else
7457  {
7458  // Success
7459  if(p != article)
7460  {
7461  if(FM_CORE == free_method) { core_free((void*) article); }
7462  else { std::free((void*) article); }
7463  article = p;
7464  free_method = FM_EXT;
7465  }
7466  // Start core to do the work
7467  rv = core_post_article(article, UI_CB_COOKIE_POST);
7468  }
7469  }
7470  }
7471  // Release memory
7472  switch(free_method)
7473  {
7474  case FM_CORE:
7475  {
7476  core_free((void*) article);
7477  break;
7478  }
7479  case FM_EXT:
7480  {
7481  ext_free((void*) article);
7482  break;
7483  }
7484  default:
7485  {
7486  std::free((void*) article);
7487  break;
7488  }
7489  }
7490  }
7491  else
7492  {
7493  // Get result
7494  core_mutex_lock();
7495  rv = data.result;
7497  }
7498 
7499  // Check return value (positive value means "in progress")
7500  if(0 >= rv)
7501  {
7502  UI_READY();
7503  if(!stateMachine(EVENT_POST_EXIT))
7504  {
7505  PRINT_ERROR("Error in main window state machine");
7506  }
7507  if(0 > rv)
7508  {
7509  UI_STATUS(S("Posting article failed."));
7510  SC("Do not use non-ASCII for the translation of this item")
7511  fl_message_title(S("Error"));
7512  fl_alert("%s", S("Posting article failed."));
7513  }
7514  else
7515  {
7516  // Success
7517  UI_STATUS(S("Article successfully posted."));
7518  if(NULL != composeWindow)
7519  {
7520  delete composeWindow;
7521  composeWindow = NULL;
7522  // Call this after EVENT_POST_EXIT
7523  composeComplete();
7524  }
7525  }
7526  }
7527 }
7528 
7529 
7530 // =============================================================================
7531 // Main window article tree search (for Message-ID)
7532 // The target article is selected, if found.
7533 //
7534 // target_mid must point to the first character after the opening angle bracket
7535 // tlen must be the size of the Message-ID without angle brackets.
7536 
7537 Fl_Tree_Item* MainWindow::searchSelectArticle(const char* target_mid,
7538  std::size_t tlen)
7539 {
7540  Fl_Tree_Item* ti;
7541  Fl_Tree_Item* oi;
7542  const char* mid;
7543  bool abort = false;
7544  int i;
7545 
7546  ti = articleTree->root();
7547  // Assignment in truth expression is intended
7548  while(NULL != (ti = articleTree->next(ti)))
7549  {
7550  mid = ((core_hierarchy_element*) ti->user_data())->header->msgid;
7551  if(std::strlen(mid) != tlen + (std::size_t) 2)
7552  {
7553  continue;
7554  }
7555  // Note: The Message-ID from header has angle brackets
7556  if(!std::strncmp(&mid[1], target_mid, tlen))
7557  {
7558  // Open branch containing target article
7559  oi = ti;
7560  while(articleTree->root() != oi)
7561  {
7562  if(!articleTree->open(oi))
7563  {
7564  articleTree->open(oi, 1);
7565  }
7566  for(i = 0; i < articleTree->root()->children(); ++i)
7567  {
7568  if(articleTree->root()->child(i) == oi)
7569  {
7570  abort = true;
7571  break;
7572  }
7573  }
7574  if(abort) { break; }
7575  oi = articleTree->prev(oi);
7576  }
7577  // Select last viewed article
7578  articleTree->set_item_focus(ti);
7579  articleTree->deselect_all();
7580  articleTree->select(ti, 1);
7581  Fl::focus(articleTree);
7582  break;
7583  }
7584  }
7585 
7586  return(ti);
7587 }
7588 
7589 
7590 // =============================================================================
7591 // Store MIME entity to file
7592 
7593 void MainWindow::storeMIMEEntityToFile(const char* uri, const char* msgid)
7594 {
7595  const char* link = NULL;
7596  const char* start = NULL;
7597  std::size_t len = 0;
7598  const char* p;
7599  unsigned int i;
7600  int error = 1;
7601  char* name = NULL;
7602  const char* suggest = NULL;
7603 
7604  // Check whether link points to our MIME entities (ignore entity index)
7605  link = gui_create_link_to_entity(msgid, 0);
7606  if(NULL != link && link[0])
7607  {
7608  // Attention: 'link' is created with angle brackets and dummy entity index
7609  start = &link[1];
7610  p = std::strrchr(start, (int) '/');
7611  if(NULL != p)
7612  {
7613  len = (size_t) (p - start);
7614  error = 0;
7615  }
7616  }
7617  if(main_debug)
7618  {
7619  if(error)
7620  {
7621  fprintf(stderr, "%s: %sCannot check link to MIME entity (bug)\n",
7622  CFG_NAME, MAIN_ERR_PREFIX);
7623  }
7624  else
7625  {
7626  fprintf(stderr, "%s: %sClick on link to MIME entity detected:\n",
7627  CFG_NAME, MAIN_ERR_PREFIX);
7628  fprintf(stderr, "%s: %s%s (URI)\n", CFG_NAME, MAIN_ERR_PREFIX, uri);
7629  fprintf(stderr, "%s: %s", CFG_NAME, MAIN_ERR_PREFIX);
7630  for(i = 0; len > (std::size_t) i; ++i)
7631  {
7632  fprintf(stderr, "%c", start[i]);
7633  }
7634  fprintf(stderr, " (Compare, Length: %u)\n", (unsigned int) len);
7635  }
7636  }
7637  // Check whether links differ before last "/"
7638  if(error || std::strncmp(uri, start, len))
7639  {
7640  // Yes (or error)
7641  SC("Do not use non-ASCII for the translation of this item")
7642  fl_message_title(S("Error"));
7643  fl_alert("%s", S("Link target not found or not supported"));
7644  }
7645  else
7646  {
7647  // Attention: Overloaded and both prototypes different than in C!
7648  p = std::strrchr(uri, (int) '/');
7649  if(NULL == p || 1 != std::sscanf(++p, "%u", &i))
7650  {
7651  PRINT_ERROR("Extracting MIME entity number from file URI failed");
7652  }
7653  else
7654  {
7655  enc_mime_cte cte = ENC_CTE_UNKNOWN;
7656  const char* entity_body = mimeData->part(i, &cte, NULL);
7657  if(NULL != entity_body)
7658  {
7659  SC("Do not use characters for the translation that cannot be")
7660  SC("converted to the ISO 8859-1 character set for this item.")
7661  SC("Leave the original string in place if in doubt.")
7662  const char* title = S("Save attachment");
7663  const char* homedir = core_get_homedir();
7664  if(NULL != homedir)
7665  {
7666  if(NULL != mimeData->filename(i))
7667  {
7668  // Use filename from Content-Disposition header field
7669  name = (char*) std::malloc(std::strlen(homedir)
7670  + std::strlen(mimeData->filename(i))
7671  + (size_t) 2);
7672  if(NULL != name)
7673  {
7674  suggest = name;
7675  std::strcpy(name, homedir);
7676  std::strcat(name, "/");
7677  std::strcat(name, mimeData->filename(i));
7678  }
7679  }
7680  else
7681  {
7682  // Use generic filename
7683  suggest = core_suggest_pathname();
7684  }
7685  if (NULL != suggest)
7686  {
7687  fl_file_chooser_ok_label(S("Save"));
7688  const char* pathname =
7689  fl_file_chooser(title, "*", suggest, 0);
7690  if(NULL != pathname)
7691  {
7692  // Convert pathname encoding to locale
7693  const char* pn =
7695  if(NULL == pn)
7696  {
7697  SC("Do not use non-ASCII for the translation of this item")
7698  fl_message_title(S("Error"));
7699  fl_alert("%s",
7700  S("Pathname conversion to locale codeset failed"));
7701  }
7702  else
7703  {
7704  // Decode Content-Transfer-Encoding and save body to file
7705  int rv = enc_mime_save_to_file(pn, cte, entity_body);
7706  if(rv)
7707  {
7708  SC("Do not use non-ASCII for the translation of this item")
7709  fl_message_title(S("Error"));
7710  fl_alert("%s", S("Operation failed"));
7711  }
7712  core_free((void*) pn);
7713  }
7714  }
7715  }
7716  core_free((void*) homedir);
7717  }
7718  }
7719  }
7720  }
7721  std::free((void*) link);
7722  std::free((void*) suggest);
7723 }
7724 
7725 
7726 // =============================================================================
7727 // Main window hyperlink handling callback
7728 
7729 void MainWindow::hyperlinkHandler(int pos)
7730 {
7731  const char* mailto = "mailto:";
7732  const char* news = "news:";
7733  const char* nntp = "nntp:";
7734  const char* file = "file:";
7735  char* uri = NULL;
7736  int rv;
7737  int i = pos;
7738  int start = pos;
7739  int end = start;
7740  std::size_t len;
7741  Fl_Tree_Item* ti;
7742  bool abort = false;
7743  std::size_t clen;
7744  std::size_t gi;
7745  core_anum_t ref_i;
7746  const char* p;
7747  char* q;
7748  int invalid;
7749 
7750  //std::printf("Debug: Hyperlink clicked at position: %d\n", pos);
7751  if(currentArticle && currentStyle)
7752  {
7753  // Extract complete URI
7754  while(i <= pos)
7755  {
7756  i = currentArticle->prev_char(i);
7757  if(currentStyle->char_at(i) == hyperlinkStyle) { start = i; }
7758  else { break; }
7759  }
7760  i = pos;
7761  while(i >= pos)
7762  {
7763  i = currentArticle->next_char(i);
7764  if(currentStyle->char_at(i) == hyperlinkStyle) { end = i; }
7765  else { ++end; break; }
7766  }
7767  uri = currentArticle->text_range(start, end);
7768  if(NULL != uri)
7769  {
7770  // Check for "nntp:" URI
7771  len = std::strlen(nntp);
7772  if(std::strlen(uri) >= len && !std::strncmp(uri, nntp, len))
7773  {
7774  // Yes => Not supported
7775  PRINT_ERROR("Multi server support missing for nntp URI type");
7776  SC("Do not use non-ASCII for the translation of this item")
7777  fl_message_title(S("Error"));
7778  fl_alert("%s", S("URI type is not supported"));
7779  }
7780  else
7781  {
7782  // Check for "news:" URI
7783  // According to RFC 1738 this specifies a newsgroup or a Message-ID
7784  // An asterisk is allowed as wildcard => This is not supported
7785  // According to RFC 5538 a server can be specified and wildmat
7786  // syntax is allowed => Both options are not supported
7787  len = std::strlen(news);
7788  if(std::strlen(uri) >= len && !std::strncmp(uri, news, len))
7789  {
7790  // Accept empty <host> for <authority>: "news:///"
7791  if(!std::strncmp(&uri[len], "///", 3))
7792  {
7793  std::memmove(&uri[len], &uri[len + (size_t) 3],
7794  std::strlen(&uri[len + (size_t) 3])
7795  + (size_t) 1);
7796  }
7797  if(std::strlen(uri) == len)
7798  {
7799  SC("Do not use non-ASCII for the translation of this item")
7800  fl_message_title(S("Error"));
7801  fl_alert("%s", S("Invalid URI format"));
7802  }
7803  // Check for unsupported format with <authority>
7804  else if(std::strchr(&uri[len], (int) '/'))
7805  {
7806  SC("Do not use non-ASCII for the translation of this item")
7807  fl_message_title(S("Error"));
7808  fl_alert("%s",
7809  S("Server specification in URI not supported"));
7810  }
7811  // Check whether URI points to newsgroup or Message-ID
7812  // Attention: Overloaded and both prototypes different than in C!
7813  else if(!std::strchr(&uri[len], (int) '@'))
7814  {
7815  // Select newsgroup
7816  if(std::strchr(&uri[len], (int) '*')
7817  || std::strchr(&uri[len], (int) '?'))
7818  {
7819  SC("Do not use non-ASCII for the translation of this item")
7820  fl_message_title(S("Error"));
7821  fl_alert("%s", S("URI doesn't specify a single group"));
7822  }
7823  else
7824  {
7825  rv = enc_percent_decode(&uri[len], 1);
7826  if(0 > rv)
7827  {
7828  SC("Do not use non-ASCII for the translation of this item")
7829  fl_message_title(S("Error"));
7830  fl_alert("%s", S("Invalid URI format"));
7831  }
7832  else
7833  {
7834  abort = true;
7835  if(group_list)
7836  {
7837  for(gi = 0; gi < group_num; ++gi)
7838  {
7839  clen = std::strlen(&uri[len]);
7840  if(std::strlen(group_list[gi].name) == clen)
7841  {
7842  if(!strncmp(&uri[len], group_list[gi].name,
7843  clen))
7844  {
7845  if((std::size_t) INT_MAX >= ++gi)
7846  {
7847  groupList->deselect();
7848  groupList->select((int) gi);
7849  groupSelect(UI_CB_START, (int) gi);
7850  abort = false;
7851  }
7852  break;
7853  }
7854  }
7855  }
7856  }
7857  }
7858  if(abort)
7859  {
7860  fl_message_title(S("Note"));
7861  rv = fl_choice("%s", S("No"),
7862  S("Yes"), NULL,
7863  // Don't wrap this line because of NLS macro!
7864  S("URI specifies a group that is not subscribed.\nSubscribe now?"));
7865  if(rv)
7866  {
7867  rv = groupStateMerge();
7868  if(!rv)
7869  {
7870  rv = core_subscribe_group(&uri[len]);
7871  if(!rv)
7872  {
7873  UI_STATUS(S("Group subscription stored."));
7874  // This check should silence code check tools
7875  // ('mainWindow' is always present)
7876  if(NULL != mainWindow)
7877  {
7878  // Import new group list
7879  mainWindow->groupListImport();
7880  }
7881  // Direct selection of the new group here is
7882  // not possible without core command queue.
7883  fl_message("%s",
7884  // Don't wrap this line because of NLS macro!
7885  S("Click URI again after operation is complete"));
7886  }
7887  }
7888  }
7889  }
7890  }
7891  }
7892  else
7893  {
7894  // Select article that corresponds to Message-ID
7895  rv = enc_percent_decode(&uri[len], 1);
7896  if(0 > rv)
7897  {
7898  SC("Do not use non-ASCII for the translation of this item")
7899  fl_message_title(S("Error"));
7900  fl_alert("%s", S("Invalid URI format"));
7901  }
7902  else
7903  {
7904  clen = std::strlen(&uri[len]);
7905  ti = searchSelectArticle(&uri[len], clen);
7906  if(NULL == ti)
7907  {
7908  // Not found in article tree
7909  if(main_debug)
7910  {
7911  printf("%s: %sTry to fetch article from URI\n",
7912  CFG_NAME, MAIN_ERR_PREFIX);
7913  }
7914  viewArticle(UI_CB_START, (const char*) &uri[len]);
7915  }
7916  }
7917  }
7918  }
7919  else
7920  {
7921  // Check for numeric value (Hyperlink to references list)
7922  clen = std::strlen(uri);
7923  if(std::strspn(uri, "0123456789") == clen)
7924  {
7925  //fl_message("%s", "Hyperlink to references list");
7926  if((std::size_t) INT_MAX >= clen && (std::size_t) 20 >= clen)
7927  {
7928  rv = enc_convert_ascii_to_anum(&ref_i, uri, (int) clen);
7929  if(!rv)
7930  {
7931  p = currentArticleHE->header->refs[ref_i];
7932  clen = std::strlen(p);
7933  q = new char[clen];
7934  std::strncpy(q, &p[1], clen - (std::size_t) 2);
7935  q[clen - (std::size_t) 2] = 0;
7936  viewArticle(UI_CB_START, q);
7937  delete[] q;
7938  }
7939  }
7940  }
7941  else
7942  {
7943  // Check for "mailto:" URI
7944  len = std::strlen(mailto);
7945  if(std::strlen(uri) >= len && !std::strncmp(uri, mailto, len))
7946  {
7947  if(std::strlen(uri) == len)
7948  {
7949  SC("Do not use non-ASCII for the translation of this item")
7950  fl_message_title(S("Error"));
7951  fl_alert("%s", S("Invalid URI format"));
7952  }
7953  else
7954  {
7955  // Yes => Use external e-mail handler
7956  // Note:
7957  // The 'mailto:' must be left in place to tell the
7958  // handler that this is an URI and not an unencoded
7959  // 'addr-spec'.
7960  rv = ext_handler_email(uri, NULL, NULL);
7961  }
7962  }
7963  else
7964  {
7965  // Check for "file:" URI
7966  len = std::strlen(file);
7967  if(std::strlen(uri) >= len
7968  && !std::strncmp(uri, file, len))
7969  {
7970  if(std::strlen(uri) == len)
7971  {
7972  SC("Do not use non-ASCII for the translation of this item")
7973  fl_message_title(S("Error"));
7974  fl_alert("%s", S("Invalid URI format"));
7975  }
7976  else
7977  {
7978  // Yes => Store MIME entity to file
7979  storeMIMEEntityToFile(uri,
7980  currentArticleHE->header->msgid);
7981  }
7982  }
7983  else
7984  {
7985  // Use external URI handler for all other clickable
7986  // links
7987  rv = ext_handler_uri(uri, &invalid);
7988  if(0 > rv && invalid)
7989  {
7990  SC("Do not use non-ASCII for the translation of this item")
7991  fl_message_title(S("Error"));
7992  fl_alert("%s", S("Invalid characters in URI"));
7993  }
7994  else if(rv)
7995  {
7996  SC("Do not use non-ASCII for the translation of this item")
7997  fl_message_title(S("Error"));
7998  fl_alert("%s", S("Starting external URI handler failed"));
7999  }
8000  }
8001  }
8002  }
8003  }
8004  }
8005  }
8006  }
8007  std::free((void*) uri);
8008 }
8009 
8010 
8011 // =============================================================================
8012 // Main window constructor
8013 //
8014 // \attention
8015 // The X display is not set yet when the main window is constructed. Therefore
8016 // anything done here is not allowed to trigger the connection to the X server.
8017 //
8018 // \param[in] label Window title
8019 
8020 MainWindow::MainWindow(const char* label) :
8021  UI_WINDOW_CLASS(730, 395, label)
8022 {
8023  const char* enabled = S("Enabled");
8024  const char* disabled = S("Disabled");
8025  const char* available = S("Available");
8026  const char* notavailable = S("Not available");
8027  Fl_Color signature = conf_integer_check(CONF_COLOR_SIGNATURE, 0x00, 0xFF);
8028  Fl_Color external = conf_integer_check(CONF_COLOR_EXTERNAL, 0x00, 0xFF);
8029  Fl_Color hyperlink = conf_integer_check(CONF_COLOR_HYPERLINK, 0x00, 0xFF);
8030  Fl_Color level1 = conf_integer_check(CONF_COLOR_LEVEL1, 0x00, 0xFF);
8031  Fl_Color level2 = conf_integer_check(CONF_COLOR_LEVEL2, 0x00, 0xFF);
8032  Fl_Color level3 = conf_integer_check(CONF_COLOR_LEVEL3, 0x00, 0xFF);
8033  Fl_Color level4 = conf_integer_check(CONF_COLOR_LEVEL4, 0x00, 0xFF);
8034  const Fl_Color styles_colors[UI_STYLES_LEN] =
8035  {
8036  FL_FOREGROUND_COLOR, // Plain bold
8037  signature, // Signature
8038  external, // External citation
8039  hyperlink, // Hyperlink
8040  // -----------------------------------------------------------------------
8041  // Keep the following group continuous
8042  FL_FOREGROUND_COLOR, // Content
8043  level1, // Thread citation level 1 ("> ")
8044  level2, // Thread citation level 2 ("> > ")
8045  level3, // Thread citation level 3 ("> > > ")
8046  level4 // Thread citation level 4 ("> > > > ")
8047  };
8048  Fl_Group* mainGroup;
8049  Fl_Group* statusGroup;
8050  Fl_Tree_Item* ti;
8051  int i;
8052  char* p;
8053 
8054  mainState = STATE_READY;
8055  startup = true;
8056  busy = false;
8057  unsub = false;
8058  wrapMode = Fl_Text_Display::WRAP_NONE;
8059  hyperlinkStyle = 0;
8060  hyperlinkPosition = -1;
8061  rot13 = false;
8062 
8063  // Init group list, current group and current article
8064  group_num = 0;
8065  group_list = NULL;
8066  group_list_index = 0;
8067  group_old = 0; // No group selected
8068  group_new = 0;
8069  subscribedGroups = NULL;
8070  currentGroup = NULL;
8071  currentArticle = NULL;
8072  currentStyle = NULL;
8073  lastArticleHE = NULL;
8074  currentArticleHE = NULL;
8075  currentLine = 0;
8076 
8077  // Init current search state
8078  p = new char[1]; p[0] = 0; // Empty string
8079  currentSearchString = p;
8080  currentSearchPosition = 0;
8081 
8082  // Init progress bar state
8083  progress_skip_update = false;
8084 
8085  // Init MIME object pointer
8086  mimeData = NULL;
8087 
8088  // Init subescribecompose window pointer
8089  subscribeWindow = NULL;
8090 
8091  // Init compose window pointer and locking (for external editor)
8092  composeWindow = NULL;
8093  composeWindowLock = 0;
8094 
8095  // Install main window close callback
8096  callback(exit_cb, (void*) this);
8097 
8098  // Always dummy use both strings to avoid "unused" warnings from optimizer
8099  aboutString << enabled << disabled << available << notavailable;
8100  aboutString.str(std::string());
8101  aboutString.clear();
8102 
8103  // Create about message
8104  aboutString << CFG_NAME << " " << CFG_VERSION
8105  << " " << S("for") << " " << CFG_OS << "\n"
8106 #if defined(CFG_MODIFIED) && CFG_MODIFIED != 0
8107  // Don't use NLS for this string because of the license terms
8108  << "(This is a modified version!)" << "\n"
8109 #endif // CFG_MODIFIED
8110  // --------------------------------------------------------------
8111  << S("Unicode version") << ": " << UC_VERSION << "\n"
8112  // --------------------------------------------------------------
8113  << "NLS: "
8114 #if CFG_USE_XSI && !CFG_NLS_DISABLE
8115  << S("Enabled") << "\n"
8116 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
8117  << S("Disabled") << "\n"
8118 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
8119  // --------------------------------------------------------------
8120  "TLS: "
8121 #if CFG_USE_TLS
8122  << S("Available") << "\n"
8123  << S("Required OpenSSL ABI") << ": "
8124 # if CFG_USE_LIBRESSL
8125  << "LibreSSL" << "\n"
8126 # else // CFG_USE_LIBRESSL
8127 # if !CFG_USE_OPENSSL_API_3 && !CFG_USE_OPENSSL_API_1_1
8128  << "1.0" << "\n"
8129 # else // !CFG_USE_OPENSSL_API_3 && !CFG_USE_OPENSSL_API_1_1
8130 # if !CFG_USE_OPENSSL_API_3
8131  << "1.1" << "\n"
8132 # else // !CFG_USE_OPENSSL_API_3
8133  << "3" << "\n"
8134 # endif // !CFG_USE_OPENSSL_API_3
8135 # endif // !CFG_USE_OPENSSL_API_3 && !CFG_USE_OPENSSL_API_1_1
8136 # endif // CFG_USE_LIBRESSL
8137 #else // CFG_USE_TLS
8138  << S("Not available") << "\n"
8139 #endif // CFG_USE_TLS
8140  // --------------------------------------------------------------
8141  << S("Required FLTK ABI") << ": " << FL_MAJOR_VERSION << "."
8142  << FL_MINOR_VERSION
8143 #ifdef FL_ABI_VERSION
8144  << "." << FL_ABI_VERSION % 100
8145 #else // FL_ABI_VERSION
8146  << "." << 0 // Not supported by FLTK 1.3.0
8147 #endif // FL_ABI_VERSION
8148  << "\n"
8149  // --------------------------------------------------------------
8150  << S("FLTK Double Buffering: ")
8151 #if CFG_DB_DISABLE
8152  << S("Disabled") << "\n"
8153 #else // CFG_DB_DISABLE
8154  << S("Enabled") << "\n"
8155 #endif // CFG_DB_DISABLE
8156  // --------------------------------------------------------------
8157  << S("Compression: ")
8158 #if CFG_CMPR_DISABLE
8159  << S("Disabled") << "\n"
8160 #else // CFG_CMPR_DISABLE
8161  << S("Available")
8162 # if CFG_USE_ZLIB
8163  << " (zlib)"
8164 # endif // CFG_USE_ZLIB
8165  << "\n"
8166 #endif // CFG_CMPR_DISABLE
8167  // --------------------------------------------------------------
8168  << "Build: " << BDATE << std::flush;
8169 
8170  // Make main window resizable
8171  resizable(this);
8172 
8173  // Add widgets --------------------------------------------------------------
8174  begin();
8175 
8176  // Main group
8177  mainGroup = new Fl_Group(0, 0, 730, 395);
8178  mainGroup->begin();
8179  {
8180  // Menu bar
8181 #if CFG_COCOA_SYS_MENUBAR
8182  menu = new Fl_Sys_Menu_Bar(0, 0, 730, 0);
8183 #else // CFG_COCOA_SYS_MENUBAR
8184  menu = new Fl_Menu_Bar(0, 0, 730, 30);
8185 #endif // CFG_COCOA_SYS_MENUBAR
8186  menu->selection_color(UI_COLOR_MENU_SELECTION);
8187 #if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
8188  // Recent FLTK 1.4 no longer shows selected entry with FL_FLAT_BOX
8189  menu->box(FL_THIN_UP_BOX);
8190 #else // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
8191  menu->box(FL_FLAT_BOX);
8192 #endif // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
8193  // --------------------
8194  SC("")
8195  SC("This section is for the main window menubar and pull-down menus.")
8196  SC("The 2 spaces after every string are intended and must be preserved.")
8197  SC("The part before the slash is the menubar entry and must be unique.")
8198  SC("The character after & is the key to pull-down a menu with 'Alt-key'.")
8199  SC("You can assign the & to any character before the slash,")
8200  SC("provided again that it is unique in the menubar!")
8201  SC("The part behind the slash is the name of the pull-down menu entry,")
8202  SC("it must only be unique inside the corresponding pull-down menu.")
8203  SC("If the name contains a slash, a submenu is created.")
8204  SC("Note:")
8205  SC("It is not possible to localize the keyboard shortcuts for the")
8206  SC("pull-down menu entries. It was implemented this way for easier")
8207  SC("documentation and for the possibility to execute blind keyboard")
8208  SC("commands in the case of a misconfigured locale.")
8209  menu->add(S("&File/Save article "), 0, asave_cb, (void*) this);
8210  menu->add(S("&File/Print article "), 0, print_cb, (void*) this);
8211  menu->add(S("&File/Quit "), "^q", exit_cb, (void*) this);
8212  // --------------------
8213  menu->add(S("&Edit/Server "), 0, server_cb, (void*) this);
8214  menu->add(S("&Edit/Configuration "), 0, config_cb, (void*) this);
8215  menu->add(S("&Edit/Identity "), 0, identity_cb, (void*) this);
8216  // --------------------
8217  i = FL_MENU_TOGGLE;
8218  if(config[CONF_TVIEW].val.i) { i |= FL_MENU_VALUE; }
8219  menu->add(S("&Group/Threaded view "), "^t", thrv_cb, (void*) this, i);
8220  i = FL_MENU_TOGGLE;
8221  if(config[CONF_TVIEW].val.i) { i |= FL_MENU_INACTIVE; }
8222  if(config[CONF_UTVIEW_AN].val.i) { i |= FL_MENU_VALUE; }
8223  menu->add(S("&Group/Sort by article number "), 0, uthrv_sort_cb,
8224  (void*) this, i);
8225  i = FL_MENU_TOGGLE;
8226  if(config[CONF_ONLYUR].val.i) { i |= FL_MENU_VALUE; }
8227  menu->add(S("&Group/Show only unread articles "), 0, onlyur_cb,
8228  (void*) this, i);
8229  menu->add(S("&Group/Subscribe "), 0, gsubscribe_cb, (void*) this);
8230  menu->add(S("&Group/Unsubscribe "), 0, gunsubscribe_cb, (void*) this);
8231  menu->add(S("&Group/Sort list "), 0, gsort_cb, (void*) this);
8232  menu->add(S("&Group/Refresh list "), "^r", grefresh_cb, (void*) this);
8233  menu->add(S("&Group/Next unread group "), "^g", nug_cb, (void*) this);
8234  menu->add(S("&Group/Mark subthread read "), 0, mssar_cb,
8235  (void*) this);
8236  menu->add(S("&Group/Mark all in group read "), "^a", maar_cb,
8237  (void*) this);
8238  menu->add(S("&Group/Mark all groups read "), 0, magar_cb,
8239  (void*) this);
8240  // --------------------
8241  menu->add(S("&Article/Search in article "), "/", asearch_cb, (void*) this);
8242  menu->add(S("&Article/Post to newsgroup "), "^p", compose_cb,
8243  (void*) this);
8244  menu->add(S("&Article/Followup to newsgroup "), "^f", reply_cb,
8245  (void*) this);
8246  menu->add(S("&Article/Supersede in newsgroup "), 0, supersede_cb,
8247  (void*) this);
8248  menu->add(S("&Article/Cancel in newsgroup "), 0, cancel_cb,
8249  (void*) this);
8250  menu->add(S("&Article/Reply by e-mail "), "^m", email_cb, (void*) this);
8251  menu->add(S("&Article/Wrap to width "), "^w", wrap_cb, (void*) this);
8252  menu->add(S("&Article/ROT13 "), "^o", rot13_cb, (void*) this);
8253  menu->add(S("&Article/Next unread article "), "^n", nua_cb,
8254  (void*) this);
8255  menu->add(S("&Article/Previous read article "), "^b", pra_cb,
8256  (void*) this);
8257  menu->add(S("&Article/View source "), "^e", viewsrc_cb, (void*) this);
8258  menu->add(S("&Article/Toggle read unread "), "^u", msau_cb,
8259  (void*) this);
8260  // --------------------
8261  i = FL_MENU_TOGGLE;
8262  if(main_debug) { i |= FL_MENU_VALUE; }
8263  menu->add(S("&Tools/Protocol console "), 0, console_cb, (void*) this);
8264  menu->add(S("&Tools/Search Message-ID "), "^s", mid_search_cb,
8265  (void*) this);
8266  // --------------------
8267  menu->add(S("&Help/About "), 0, about_cb, (void*) this);
8268  menu->add(S("&Help/Message of the day "), 0, viewmotd_cb, (void*) this);
8269  menu->add(S("&Help/License "), 0, license_cb, (void*) this);
8270  menu->add(S("&Help/Bug report "), 0, bug_cb, (void*) this);
8271  SC("")
8272  // --------------------
8273 
8274  // Content group
8275 #if CFG_COCOA_SYS_MENUBAR
8276  contentGroup = new Fl_Tile(0, 0, 730, 370);
8277 #else // CFG_COCOA_SYS_MENUBAR
8278  contentGroup = new Fl_Tile(0, 30, 730, 340);
8279 #endif // CFG_COCOA_SYS_MENUBAR
8280  contentGroup->begin();
8281  {
8282  // Group list
8283 #if CFG_COCOA_SYS_MENUBAR
8284  groupList = new My_Multi_Browser(0, 0, 230, 370);
8285 #else // CFG_COCOA_SYS_MENUBAR
8286  groupList = new My_Multi_Browser(0, 30, 230, 340);
8287 #endif // CFG_COCOA_SYS_MENUBAR
8288  groupList->callback(gselect_cb, (void*) this);
8289 #if CFG_COCOA_SYS_MENUBAR
8290  contentGroup2 = new Fl_Tile(230, 0, 500, 370);
8291 #else // CFG_COCOA_SYS_MENUBAR
8292  contentGroup2 = new Fl_Tile(230, 30, 500, 340);
8293 #endif // CFG_COCOA_SYS_MENUBAR
8294  contentGroup2->begin();
8295  {
8296  // Article tree
8297 #if CFG_COCOA_SYS_MENUBAR
8298  articleTree = new My_Tree(230, 0, 500, 140);
8299 #else // CFG_COCOA_SYS_MENUBAR
8300  articleTree = new My_Tree(230, 30, 500, 140);
8301 #endif // CFG_COCOA_SYS_MENUBAR
8302  articleTree->showroot(0);
8303  articleTree->connectorstyle(FL_TREE_CONNECTOR_SOLID);
8304  articleTree->connectorcolor(FL_FOREGROUND_COLOR);
8305  // Explicitly set foreground color to FLTK default
8306  // (default is not the same as for the other widgets)
8307  articleTree->item_labelfgcolor(FL_FOREGROUND_COLOR);
8308  ti = articleTree->add(S("No articles"));
8309  if(NULL != ti) { ti->deactivate(); }
8310  articleTree->callback(aselect_cb, (void*) this);
8311  // Article content window
8312 #if CFG_COCOA_SYS_MENUBAR
8313  text = new My_Text_Display(230, 140, 500, 230);
8314 #else // CFG_COCOA_SYS_MENUBAR
8315  text = new My_Text_Display(230, 170, 500, 200);
8316 #endif // CFG_COCOA_SYS_MENUBAR
8317  text->callback(hyperlink_cb, (void*) this);
8318  text->textfont(FL_COURIER);
8319  }
8320  contentGroup2->end();
8321  }
8322  contentGroup->end();
8323 
8324  // Status group
8325  statusGroup = new Fl_Group(0, 370, 730, 25);
8326  statusGroup->begin();
8327  {
8328  // Progress bar
8329  progressBar = new Fl_Progress(0, 370, 100, 25);
8330  progressBar->color(FL_BACKGROUND_COLOR, UI_COLOR_PROGRESS_BAR);
8331  progressBar->minimum(0.0);
8332  progressBar->maximum(100.0);
8333  progressBar->value(0.0);
8334  progressBar->label("");
8335  // Status bar
8336  statusBar = new Fl_Box(100, 370, 730, 25);
8337  statusBar->box(FL_DOWN_BOX);
8338  statusBar->align(FL_ALIGN_INSIDE | FL_ALIGN_LEFT);
8339  statusBar->label("");
8340  }
8341  statusGroup->end();
8342  statusGroup->resizable(statusBar);
8343  }
8344  mainGroup->end();
8345  mainGroup->resizable(contentGroup);
8346 
8347  end();
8348  // --------------------------------------------------------------------------
8349 
8350  // Allocate and init styles
8351  styles_len = UI_STYLES_LEN;
8352  styles = new Fl_Text_Display::Style_Table_Entry[styles_len];
8353  styles[0].color = FL_FOREGROUND_COLOR;
8354  styles[0].font = FL_COURIER_BOLD;
8355  styles[0].size = text->textsize();
8356  styles[0].attr = 0;
8357  for(i = 1; i < styles_len; ++i)
8358  {
8359  styles[i].color = styles_colors[i];
8360  styles[i].font = text->textfont();
8361  styles[i].size = text->textsize();
8362  styles[i].attr = 0;
8363  }
8364 }
8365 
8366 
8367 // =============================================================================
8368 // Main window destructor
8369 //
8370 // \attention
8371 // The FLTK documentation specifies that 'highlight_data()' can remove a style
8372 // buffer, but it's unclear how this should work. Passing 'NULL' is not allowed
8373 // because the pointer to the new style buffer is always dereferenced.
8374 // For 'buffer()' it is not explicitly allowed to pass 'NULL' in the
8375 // documentation too. Even if it works this may change in future releases.
8376 // As a workaround we assign a dummy text buffer before destroying the attached
8377 // one.
8378 
8379 MainWindow::~MainWindow(void)
8380 {
8381  if(NULL != subscribedGroups)
8382  {
8383  core_destroy_subscribed_group_info(&subscribedGroups);
8384  }
8385  if(NULL != mimeData) { delete mimeData; }
8386  delete[] currentSearchString;
8387  core_free(currentGroup);
8388  core_destroy_subscribed_group_states(&group_num, &group_list);
8389  // Detach highlight data from 'text' before destroying it
8390  text->highlight_data(dummyTb, NULL, 0, 'A', NULL, NULL);
8391  if(currentStyle) { delete currentStyle; }
8392  delete[] styles;
8393  // Detach current article from 'text' before destroying it
8394  text->buffer(dummyTb);
8395  if(currentArticle) { delete currentArticle; }
8396 }
8397 
8398 
8399 // =============================================================================
8400 // Server configuration window UI driving
8401 
8402 int ServerCfgWindow::process(void)
8403 {
8404  // Drive UI until user press OK or Cancel button
8405  while(!finished) { Fl::wait(); }
8406 
8407  // Copy data back to server configuration object
8408  sconf->serverReplace(scfgHostname->value());
8409  sconf->serviceReplace(scfgService->value());
8410  if(scfgTlsStrong->value()) { sconf->enc = UI_ENC_STRONG; }
8411  else if(scfgTlsWeak->value()) { sconf->enc = UI_ENC_WEAK; }
8412  else { sconf->enc = UI_ENC_NONE; }
8413  if(scfgAuthUser->value()) { sconf->auth = UI_AUTH_USER; }
8414  else { sconf->auth = UI_AUTH_NONE; }
8415 
8416  return(finished);
8417 }
8418 
8419 
8420 // =============================================================================
8421 // Server configuration window constructor
8422 
8423 ServerCfgWindow::ServerCfgWindow(ServerConfig* sc, const char* label) :
8424  UI_WINDOW_CLASS(700, 395, label)
8425 {
8426  Fl_Group* gp;
8427  Fl_Box* bp;
8428  Fl_Button* p;
8429  int y, w, h, gy, gh, bw;
8430  int w1, w2, h1, h2;
8431  std::ostringstream serverString;
8432  std::ostringstream serviceString;
8433  std::ostringstream tlsString;
8434  Fl_Group* grpTls;
8435 
8436  // Configure server window
8437  sconf = sc;
8438  finished = 0;
8439  copy_label(label);
8440  set_modal();
8441  callback(cancel_cb, (void*) this);
8442 
8443  // Prepare label for server hostname input field
8444  serverString << S("NNTP server hostname")
8445  << " (" << S("or IP address") << ")" << std::flush;
8446  // Prepare label for server service input field
8447  serviceString << S("Service name")
8448  << " (" << S("or TCP port") << ")" << std::flush;
8449  tlsString << "Transport Layer Security (TLS)"
8450  << " " << S("and server to client authentication") << std::flush;
8451 
8452  // Attention:
8453  //
8454  // const char* server = serverString.str().c_str()
8455  //
8456  // creates 'server' with undefined value because the compiler is
8457  // allowed to free the temporary string object returned by 'str()'
8458  // immediately after the assignment!
8459  // Assigning names to temporary string objects forces them to stay in
8460  // memory as long as their names go out of scope (this is what we
8461  // need).
8462  const std::string& srv_s = serverString.str();
8463  const std::string& svc_s = serviceString.str();
8464  const std::string& tls_s = tlsString.str();
8465 
8466  // Copy C strings
8467  hostname = new char[std::strlen(srv_s.c_str()) + (std::size_t) 1];
8468  std::strcpy(hostname, srv_s.c_str());
8469  service = new char[std::strlen(svc_s.c_str()) + (std::size_t) 1];
8470  std::strcpy(service, svc_s.c_str());
8471  tls_headline = new char[std::strlen(tls_s.c_str()) + (std::size_t) 1];
8472  std::strcpy(tls_headline, tls_s.c_str());
8473 
8474  // Create widget group
8475  scfgGroup = new Fl_Group(0, 0, 700, 395);
8476  scfgGroup->begin();
8477  {
8478  // Calculate required widths and heights
8479  gui_set_default_font();
8480  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
8481  fl_measure(S("OK"), w2 = 0, h2 = 0);
8482  if(w1 > w2) { w = w1; } else { w = w2; }
8483  if(h1 > h2) { h = h1; } else { h = h2; }
8484  w += 30;
8485  h += 10;
8486  gh = h + 10;
8487  gy = 395 - gh;
8488  // Add server hostname field
8489  scfgHostname = new Fl_Input(15, 35, 450, 30, hostname);
8490  scfgHostname->align(FL_ALIGN_TOP_LEFT);
8491  scfgHostname->value(sc->server);
8492  // Add server TCP port field
8493  scfgService = new Fl_Input(480, 35, 205, 30, service);
8494  scfgService->align(FL_ALIGN_TOP_LEFT);
8495  scfgService->value(sc->service);
8496  // Add TLS radio buttons
8497  grpTls = new Fl_Group(15, 100, 670, 120, tls_headline);
8498  grpTls->begin();
8499  {
8500  scfgTlsOff = new Fl_Radio_Round_Button(30, 115, 640, 30,
8501  S("Disabled"));
8502  scfgTlsOff->selection_color(UI_COLOR_RADIO_BUTTON);
8503  scfgTlsOff->callback(enc_off_cb, (void*) this);
8504  scfgTlsStrong = new Fl_Radio_Round_Button(30, 145, 640, 30,
8505  S("Enabled with strong encryption and forward secrecy"));
8506  scfgTlsStrong->selection_color(UI_COLOR_RADIO_BUTTON);
8507  scfgTlsStrong->callback(enc_on_cb, (void*) this);
8508  scfgTlsWeak = new Fl_Radio_Round_Button(30, 175, 640, 30,
8509  S("Enabled in compatibility mode offering weak cipher suites"));
8510  scfgTlsWeak->selection_color(UI_COLOR_RADIO_BUTTON);
8511  scfgTlsWeak->callback(enc_on_cb, (void*) this);
8512  }
8513  grpTls->end();
8514  grpTls->align(FL_ALIGN_TOP_LEFT);
8515  grpTls->box(FL_EMBOSSED_BOX);
8516 #if !CFG_USE_TLS
8517  grpTls->deactivate();
8518  sc->enc = UI_ENC_NONE;
8519 #endif // CFG_USE_TLS
8520  switch(sc->enc)
8521  {
8522  case UI_ENC_STRONG: { scfgTlsStrong->set(); break; }
8523  case UI_ENC_WEAK: { scfgTlsWeak->set(); break; }
8524  default: { scfgTlsOff->set(); break; }
8525  }
8526  // Add authentication radio buttons
8527  grpAuth = new Fl_Group(15, 255, 670, 90,
8528  S("Client to server authentication"));
8529  grpAuth->begin();
8530  {
8531  scfgAuthOff = new Fl_Radio_Round_Button(30, 270, 640, 30,
8532  S("Disabled"));
8533  scfgAuthOff->selection_color(UI_COLOR_RADIO_BUTTON);
8534  scfgAuthUser = new Fl_Radio_Round_Button(30, 300, 640, 30,
8535  S("AUTHINFO USER/PASS as defined in RFC 4643"));
8536  scfgAuthUser->selection_color(UI_COLOR_RADIO_BUTTON);
8537  }
8538  grpAuth->end();
8539  grpAuth->align(FL_ALIGN_TOP_LEFT);
8540  grpAuth->box(FL_EMBOSSED_BOX);
8541 #if !CFG_NNTP_AUTH_UNENCRYPTED
8542  if(UI_ENC_NONE == sc->enc)
8543  {
8544  scfgAuthOff->set();
8545  grpAuth->deactivate();
8546  }
8547  else
8548 #endif
8549  {
8550  switch(sc->auth)
8551  {
8552  case UI_AUTH_USER: { scfgAuthUser->set(); break; }
8553  default: { scfgAuthOff->set(); break; }
8554  }
8555  }
8556  // Add button bar at bottom
8557  gp = new Fl_Group(0, gy, 700, gh);
8558  gp->begin();
8559  {
8560  bw = w + 30;
8561  y = gy + 5;
8562  new Fl_Box(0, gy, bw, gh);
8563  p = new Fl_Button(15, y, w, h, S("OK"));
8564  p->callback(ok_cb, (void*) this);
8565  new Fl_Box(500 - bw, gy, bw, gh);
8566  p = new Fl_Button(700 - bw + 15, y, w, h, S("Cancel"));
8567  p->callback(cancel_cb, (void*) this);
8568  // Resizable space between buttons
8569  bp = new Fl_Box(bw, gy, 700 - (2 * bw), gh);
8570  }
8571  gp->end();
8572  gp->resizable(bp);
8573  }
8574  scfgGroup->end();
8575  resizable(scfgGroup);
8576  // Set fixed window size
8577  size_range(700, 395, 700, 395);
8578 #if !CFG_DB_DISABLE
8579  Fl::visual(FL_DOUBLE | FL_INDEX);
8580 #endif // CFG_DB_DISABLE
8581  show();
8582 }
8583 
8584 
8585 // =============================================================================
8586 // Server configuration window destructor
8587 
8588 ServerCfgWindow::~ServerCfgWindow(void)
8589 {
8590  delete[] hostname;
8591  delete[] service;
8592  delete[] tls_headline;
8593 }
8594 
8595 
8596 // =============================================================================
8597 // Identity configuration window callback
8598 //
8599 // Only store the Unicode data here, the encoding is done elsewhere.
8600 
8601 void IdentityCfgWindow::ok_cb_i(void)
8602 {
8603  std::size_t len1;
8604  std::size_t len2;
8605  char* buf;
8606 
8607  // Store "From" header in Unicode format (without MIME encoding)
8608  len1 = std::strlen(fromName->value());
8609  len2 = std::strlen(fromEmail->value());
8610  if(UI_HDR_BUFSIZE <= len1 || UI_HDR_BUFSIZE <= len2)
8611  {
8612  SC("Do not use non-ASCII for the translation of this item")
8613  fl_message_title(S("Error"));
8614  fl_alert("%s", S("From header field element too long"));
8615  }
8616  buf = new char[len1 + len2 + (std::size_t) 4];
8617  buf[0] = 0;
8618  if(len2)
8619  {
8620  if(len1) { std::strcat(buf, fromName->value()); }
8621  std::strcat(buf, " <");
8622  std::strcat(buf, fromEmail->value());
8623  std::strcat(buf, ">");
8624  }
8626  delete[] buf;
8627 
8628  // Store body for "Reply-To" header field
8629  len1 = std::strlen(replytoName->value());
8630  len2 = std::strlen(replytoEmail->value());
8631  if(UI_HDR_BUFSIZE <= len1 || UI_HDR_BUFSIZE <= len2)
8632  {
8633  SC("Do not use non-ASCII for the translation of this item")
8634  fl_message_title(S("Error"));
8635  fl_alert("%s", S("Reply-To header field element too long"));
8636  }
8637  buf = new char[len1 + len2 + (std::size_t) 4];
8638  buf[0] = 0;
8639  if(len2)
8640  {
8641  if(len1) { std::strcat(buf, replytoName->value()); }
8642  std::strcat(buf, " <");
8643  std::strcat(buf, replytoEmail->value());
8644  std::strcat(buf, ">");
8645  }
8646  if(std::strcmp(buf, config[CONF_FROM].val.s))
8647  {
8649  }
8650  else { conf_string_replace(&config[CONF_REPLYTO], ""); }
8651  delete[] buf;
8652 
8653  // Destroy configuration window
8654  Fl::delete_widget(this);
8655 }
8656 
8657 
8658 // =============================================================================
8659 // Identity configuration window constructor
8660 
8661 IdentityCfgWindow::IdentityCfgWindow(const char* label) :
8662  UI_WINDOW_CLASS(700, 200, label)
8663 {
8664  char from_name[UI_HDR_BUFSIZE + (std::size_t) 1];
8665  char from_email[UI_HDR_BUFSIZE + (std::size_t) 1];
8666  char replyto_name[UI_HDR_BUFSIZE + (std::size_t) 1];
8667  char replyto_email[UI_HDR_BUFSIZE + (std::size_t) 1];
8668  std::size_t i;
8669  Fl_Group* gp1;
8670  Fl_Group* gp2;
8671  Fl_Box* bp;
8672  Fl_Button* p;
8673  int y, w, h, gy, gh, bw;
8674  int w1, w2, h1, h2;
8675 
8676  // Configure identity window
8677  copy_label(label);
8678  set_modal();
8679  callback(cancel_cb, (void*) this);
8680 
8681  // Split current identity configuration into names and e-mail addresses
8682  std::strncpy(from_name, config[CONF_FROM].val.s, UI_HDR_BUFSIZE);
8683  from_name[UI_HDR_BUFSIZE] = 0;
8684  from_email[0] = 0;
8685  if(std::strlen(from_name))
8686  {
8687  i = 0;
8688  while(from_name[i])
8689  {
8690  if('<' == from_name[i])
8691  {
8692  // Attention: Overloaded and both prototypes different than in C!
8693  if(NULL == std::strchr(&from_name[i + (std::size_t) 1], (int) '<'))
8694  {
8695  // Remaining data doesn't contain another opening angle bracket
8696  if(!i || ((std::size_t) 1 == i && ' ' == from_name[0]))
8697  {
8698  from_name[0] = 0;
8699  }
8700  else { from_name[i - (std::size_t) 1] = 0; }
8701  std::strncpy(from_email, &from_name[++i], UI_HDR_BUFSIZE);
8702  i = std::strlen(from_email) - (std::size_t) 1;
8703  if('>' != from_email[i]) { from_email[0] = 0; }
8704  else { from_email[i] = 0; }
8705  break;
8706  }
8707  }
8708  ++i;
8709  }
8710  }
8711  std::strncpy(replyto_name, config[CONF_REPLYTO].val.s, UI_HDR_BUFSIZE);
8712  replyto_name[UI_HDR_BUFSIZE] = 0;
8713  replyto_email[0] = 0;
8714  if(std::strlen(replyto_name))
8715  {
8716  i = 0;
8717  while(replyto_name[i])
8718  {
8719  if('<' == replyto_name[i])
8720  {
8721  if(!i || ((std::size_t) 1 == i && ' ' == replyto_name[0]))
8722  {
8723  replyto_name[0] = 0;
8724  }
8725  else { replyto_name[i - (std::size_t) 1] = 0; }
8726  std::strncpy(replyto_email, &replyto_name[++i], UI_HDR_BUFSIZE);
8727  i = std::strlen(replyto_email) - (std::size_t) 1;
8728  if('>' != replyto_email[i]) { replyto_email[0] = 0; }
8729  else { replyto_email[i] = 0; }
8730  break;
8731  }
8732  else { ++i; }
8733  }
8734  }
8735 
8736  // Create widget group
8737  cfgGroup = new Fl_Group(0, 0, 700, 200);
8738  cfgGroup->begin();
8739  {
8740  // Calculate required widths and heights
8741  gui_set_default_font();
8742  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
8743  fl_measure(S("OK"), w2 = 0, h2 = 0);
8744  if(w1 > w2) { w = w1; } else { w = w2; }
8745  if(h1 > h2) { h = h1; } else { h = h2; }
8746  w += 30;
8747  h += 10;
8748  gh = h + 10;
8749  gy = 200 - gh;
8750 
8751  gp1 = new Fl_Group(0, 0, 700, gy);
8752  gp1->begin();
8753  {
8754  // Add "From" Name field
8755  fromName = new Fl_Input(15, 35, 325, 30, "From (Name):");
8756  fromName->align(FL_ALIGN_TOP_LEFT);
8757  fromName->value(from_name);
8758  // Add "From" e-mail field
8759  fromEmail = new Fl_Input(355, 35, 325, 30, "From (e-mail):");
8760  fromEmail->align(FL_ALIGN_TOP_LEFT);
8761  fromEmail->value(from_email);
8762  // Add "Reply-To" Name field
8763  replytoName = new Fl_Input(15, 100, 325, 30, "Reply-To (Name):");
8764  replytoName->align(FL_ALIGN_TOP_LEFT);
8765  replytoName->value(replyto_name);
8766  // Add "Reply-To" e-mail field
8767  replytoEmail = new Fl_Input(355, 100, 325, 30, "Reply-To (e-mail):");
8768  replytoEmail->align(FL_ALIGN_TOP_LEFT);
8769  replytoEmail->value(replyto_email);
8770  // Resizable space below input fields
8771  bp = new Fl_Box(0, 150, 700, gy - 150);
8772  //bp->box(FL_DOWN_BOX);
8773  }
8774  gp1->end();
8775  gp1->resizable(bp);
8776  // Add button bar at bottom
8777  gp2 = new Fl_Group(0, gy, 700, gh);
8778  gp2->begin();
8779  {
8780  bw = w + 30;
8781  y = gy + 5;
8782  new Fl_Box(0, gy, bw, gh);
8783  p = new Fl_Button(15, y, w, h, S("OK"));
8784  p->callback(ok_cb, (void*) this);
8785  new Fl_Box(500 - bw, gy, bw, gh);
8786  p = new Fl_Button(700 - bw + 15, y, w, h, S("Cancel"));
8787  p->callback(cancel_cb, (void*) this);
8788  // Resizable space between buttons
8789  bp = new Fl_Box(bw, gy, 700 - (2 * bw), gh);
8790  }
8791  gp2->end();
8792  gp2->resizable(bp);
8793  }
8794  cfgGroup->end();
8795  cfgGroup->resizable(gp1);
8796  resizable(cfgGroup);
8797  // Set minimum window size
8798  size_range(700, 150 + gh, 0, 0);
8799 #if !CFG_DB_DISABLE
8800  Fl::visual(FL_DOUBLE | FL_INDEX);
8801 #endif // CFG_DB_DISABLE
8802  show();
8803 }
8804 
8805 
8806 // =============================================================================
8807 // Identity configuration window destructor
8808 
8809 IdentityCfgWindow::~IdentityCfgWindow(void)
8810 {
8811 }
8812 
8813 
8814 // =============================================================================
8815 // Miscellaneous configuration window callback
8816 
8817 void MiscCfgWindow::ok_cb_i(void)
8818 {
8819  const char* is;
8820  const unsigned int cac_limit = (unsigned int) UI_CAC_MAX;
8821  unsigned int i;
8822  int error = 1;
8823 
8824  // Update CAC configuration
8825  is = cacField->value();
8826  if(NULL != is && 1 == std::sscanf(is, "%u", &i))
8827  {
8828  if(cac_limit < i) { i = cac_limit; }
8829  config[CONF_CAC].val.i = (int) i;
8830  error = 0;
8831  }
8832  else
8833  {
8834  SC("Do not use non-ASCII for the translation of this item")
8835  fl_message_title(S("Error"));
8836  fl_alert("%s", S("Invalid value for CAC"));
8837  }
8838 
8839  // Update checkbox settings
8840  if(!error)
8841  {
8842  if(cmprEnable->value()) { config[CONF_COMPRESSION].val.i = 1; }
8843  else { config[CONF_COMPRESSION].val.i = 0; }
8844  if(localTime->value()) { config[CONF_TS_LTIME].val.i = 1; }
8845  else { config[CONF_TS_LTIME].val.i = 0; }
8846  if(uagentEnable->value()) { config[CONF_ENABLE_UAGENT].val.i = 1; }
8847  else { config[CONF_ENABLE_UAGENT].val.i = 0; }
8848 
8849  if(qsSpace->value()) { config[CONF_QUOTESTYLE].val.i = 1; }
8850  else { config[CONF_QUOTESTYLE].val.i = 0; }
8851  if(qsUnify->value()) { config[CONF_QUOTEUNIFY].val.i = 1; }
8852  else { config[CONF_QUOTEUNIFY].val.i = 0; }
8853  }
8854 
8855  // Destroy misc configuration window if no error was detected
8856  if(!error) { Fl::delete_widget(this); }
8857 }
8858 
8859 
8860 // =============================================================================
8861 // Miscellaneous configuration window constructor
8862 
8863 MiscCfgWindow::MiscCfgWindow(const char* label) :
8864  UI_WINDOW_CLASS(400, 235, label)
8865 {
8866  Fl_Group* gp1;
8867  Fl_Group* gp2;
8868  Fl_Box* bp;
8869  Fl_Button* p;
8870  int th = 30; // Tab hight (including gap)
8871  int tg = 5; // Gap between tabs and cards
8872  int y, w, h, gy, gh, bw;
8873  int w1, w2, h1, h2;
8874  std::ostringstream ss;
8875  // Label strings
8876  SC("Preserve the spaces at beginning and end")
8877  const char* label_tab_cac = S(" Misc ");
8878  const char* label_tab_qs = S(" Quote style ");
8879  SC("Preserve the colon")
8880  const char* label_cac = S("Clamp article count threshold:");
8881 
8882  // Configure config window
8883  copy_label(label);
8884  set_modal();
8885  callback(cancel_cb, (void*) this);
8886 
8887  // Create widget group
8888  gp1 = new Fl_Group(0, 0, 400, 235);
8889  gp1->begin();
8890  {
8891  // Calculate required widths and heights
8892  gui_set_default_font();
8893  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
8894  fl_measure(S("OK"), w2 = 0, h2 = 0);
8895  if(w1 > w2) { w = w1; } else { w = w2; }
8896  if(h1 > h2) { h = h1; } else { h = h2; }
8897  w += 30;
8898  h += 10;
8899  gh = h + 10;
8900  gy = 235 - gh;
8901 
8902  cfgTabs = new Fl_Tabs(0, 0, 400, gy);
8903  cfgTabs->begin();
8904  {
8905  cacGroup = new Fl_Group(0, th - tg, 400, 235 - th + tg, label_tab_cac);
8906  cacGroup->begin();
8907  {
8908  cacField = new Fl_Input(15, 55, 400 - 30, 30, label_cac);
8909  cacField->align(FL_ALIGN_TOP_LEFT);
8910  // Note: We cannot use 'posix_snprintf()' here
8911  ss << config[CONF_CAC].val.i << std::flush;
8912  const std::string& s = ss.str();
8913  cacField->value(s.c_str());
8914  cmprEnable = new Fl_Check_Button(15, 90, 400 - 30, 30,
8915  S("Negotiate compression if available"));
8916  cmprEnable->tooltip(
8917  SC("This is the tooltip for the compression negotiation checkbox")
8918  S("Not recommended when confidential newsgroups are accessed")
8919  );
8920  cmprEnable->value(config[CONF_COMPRESSION].val.i);
8921 #if CFG_CMPR_DISABLE
8922  cmprEnable->deactivate();
8923 #endif // CFG_CMPR_DISABLE
8924  localTime = new Fl_Check_Button(15, 120, 400 - 30, 30,
8925  S("Use localtime for Date header field"));
8926  localTime->tooltip(
8927  SC("This is the tooltip for the use localtime checkbox")
8928  S("Using localtime is recommended by RFC 5322")
8929  );
8930  localTime->value(config[CONF_TS_LTIME].val.i);
8931  uagentEnable = new Fl_Check_Button(15, 150, 400 - 30, 30,
8932  S("Add User-Agent header field"));
8933  uagentEnable->value(config[CONF_ENABLE_UAGENT].val.i);
8934  // Resizable space below input fields
8935  bp = new Fl_Box(0, 180, 400, gy - 150);
8936  //bp->box(FL_DOWN_BOX);
8937  }
8938  cacGroup->end();
8939  cacGroup->resizable(bp);
8940 
8941  // --------------------------------------------------------------------
8942 
8943  qsGroup = new Fl_Group(0, th - tg, 400, 235 - th + tg, label_tab_qs);
8944  qsGroup->begin();
8945  {
8946  qsSpace = new Fl_Check_Button(15, 45, 400 - 30, 30,
8947  S("Space after quote marks"));
8948  qsSpace->value(config[CONF_QUOTESTYLE].val.i);
8949  qsUnify = new Fl_Check_Button(15, 75, 400 - 30, 30,
8950  S("Unify quote marks for follow-up"));
8951  qsUnify->value(config[CONF_QUOTEUNIFY].val.i);
8952  // Resizable space below input fields
8953  bp = new Fl_Box(0, 150, 400, gy - 150);
8954  //bp->box(FL_DOWN_BOX);
8955  }
8956  qsGroup->end();
8957  qsGroup->resizable(bp);
8958  }
8959  cfgTabs->end();
8960  cfgTabs->resizable(cacGroup);
8961  // Add button bar at bottom
8962  gp2 = new Fl_Group(0, gy, 400, gh);
8963  gp2->begin();
8964  {
8965  bw = w + 30;
8966  y = gy + 5;
8967  new Fl_Box(0, gy, bw, gh);
8968  p = new Fl_Button(15, y, w, h, S("OK"));
8969  p->callback(ok_cb, (void*) this);
8970  new Fl_Box(500 - bw, gy, bw, gh);
8971  p = new Fl_Button(400 - bw + 15, y, w, h, S("Cancel"));
8972  p->callback(cancel_cb, (void*) this);
8973  // Resizable space between buttons
8974  bp = new Fl_Box(bw, gy, 400 - (2 * bw), gh);
8975  }
8976  gp2->end();
8977  gp2->resizable(bp);
8978  }
8979  gp1->end();
8980  gp1->resizable(cfgTabs);
8981  resizable(gp1);
8982  // Set minimum window size
8983  size_range(400, 235, 0, 0);
8984 #if !CFG_DB_DISABLE
8985  Fl::visual(FL_DOUBLE | FL_INDEX);
8986 #endif // CFG_DB_DISABLE
8987  show();
8988 }
8989 
8990 
8991 // =============================================================================
8992 // Miscellaneous configuration window destructor
8993 
8994 MiscCfgWindow::~MiscCfgWindow(void)
8995 {
8996 }
8997 
8998 
8999 // =============================================================================
9000 // Search window callback
9001 
9002 void SearchWindow::ok_cb_i(void)
9003 {
9004  const char* ss;
9005  const char* ss_nfc;
9006  int len;
9007  std::size_t tmp;
9008  char* p;
9009 
9010  // Check search string
9011  ss = searchField->value();
9012  // Convert search string to Unicode Normalization Form C
9013  ss_nfc = enc_convert_to_utf8_nfc(ENC_CS_UTF_8, ss);
9014  if(NULL == ss_nfc)
9015  {
9016  UI_STATUS(S("Search failed."));
9017  SC("Do not use non-ASCII for the translation of this item")
9018  fl_message_title(S("Error"));
9019  fl_alert("%s", S("Processing Unicode data in search string failed"));
9020  }
9021  else
9022  {
9023  if(std::strcmp(*currentSearchString, ss_nfc))
9024  {
9025  // Search string has changed
9026  tmp = std::strlen(ss_nfc);
9027  if((std::size_t) INT_MAX <= tmp) { len = INT_MAX - 1; }
9028  else { len = (int) tmp; }
9029  // Replace current search string
9030  delete[] *currentSearchString;
9031  p = new char[len + 1];
9032  std::strncpy(p, ss_nfc, (size_t) len);
9033  p[len] = 0;
9034  *currentSearchString = p;
9035  }
9036  // Release memory for normalized string (if allocated)
9037  if(ss != ss_nfc) { enc_free((void*) ss_nfc); }
9038 
9039  // Check for case-insensitive search request
9040  config[CONF_SEARCH_CASE_IS].val.i = cisEnable->value();
9041  }
9042 }
9043 
9044 
9045 // =============================================================================
9046 // Search window constructor
9047 
9048 SearchWindow::SearchWindow(const char* label, const char** oldSearchString) :
9049  UI_WINDOW_CLASS(400, 165, label)
9050 {
9051  Fl_Group* gp; // Toplevel group in window
9052  Fl_Group* searchGroup;
9053  Fl_Group* searchGroup2;
9054  Fl_Group* buttonGroup;
9055  Fl_Box* bp;
9056  Fl_Button* p;
9057  int y, w, h, gy, gh, bw;
9058  int w1, w2, h1, h2;
9059  std::ostringstream ss;
9060  // Label strings
9061  SC("Preserve the colon")
9062  const char* label_search = S("Search for:");
9063 
9064  // Init finished flag
9065  finished = 0;
9066 
9067  // Init pointer to current search string
9068  currentSearchString = oldSearchString;
9069 
9070  // Configure config window
9071  copy_label(label);
9072  set_modal();
9073  callback(cancel_cb, (void*) this);
9074 
9075  // Create widget group
9076  gp = new Fl_Group(0, 0, 400, 165);
9077  gp->begin();
9078  {
9079  // Calculate required widths and heights
9080  gui_set_default_font();
9081  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
9082  fl_measure(S("OK"), w2 = 0, h2 = 0);
9083  if(w1 > w2) { w = w1; } else { w = w2; }
9084  if(h1 > h2) { h = h1; } else { h = h2; }
9085  w += 30;
9086  h += 10;
9087  gh = h + 10; // Height of bottom button bar
9088  gy = 165 - gh; // Height of top group
9089  searchGroup = new Fl_Group(0, 0, 400, 165 - gh);
9090  searchGroup->begin();
9091  {
9092  // Wrap a separate group around the input field
9093  searchGroup2 = new Fl_Group(0, 0, 400, 80);
9094  searchGroup2->begin();
9095  {
9096  // Input field in inner group
9097  searchField = new Fl_Input(15, 35, 400 - 30, 30, label_search);
9098  searchField->align(FL_ALIGN_TOP_LEFT);
9099  // Note: We cannot use 'posix_snprintf()' here
9100  ss << *currentSearchString << std::flush;
9101  const std::string& s = ss.str();
9102  searchField->value(s.c_str());
9103  searchField
9104 #if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
9105  // Requires FLTK 1.4.0
9106  ->insert_position
9107 #else // defined FL_ABI_VERSION && 10303 <= FL_ABI_VERSION
9108  ->position
9109 #endif // defined FL_ABI_VERSION && 10303 <= FL_ABI_VERSION
9110  (searchField->size(), 0);
9111  searchField->take_focus();
9112  }
9113  searchGroup2->end();
9114  // Check button in outer group
9115  cisEnable = new Fl_Check_Button(15, 80, 400 - 30, 30,
9116  S("Search case-insensitive"));
9117  cisEnable->value(config[CONF_SEARCH_CASE_IS].val.i);
9118  cisEnable->clear_visible_focus();
9119  // Resizable space below input fields
9120  bp = new Fl_Box(0, 110, 400, gy - 110);
9121  //bp->box(FL_DOWN_BOX);
9122  //bp->color(FL_GREEN);
9123  }
9124  searchGroup->end();
9125  searchGroup->resizable(bp);
9126  // Add button bar at bottom
9127  buttonGroup = new Fl_Group(0, gy, 400, gh);
9128  buttonGroup->begin();
9129  {
9130  bw = w + 30;
9131  y = gy + 5;
9132  new Fl_Box(0, gy, bw, gh);
9133  p = new Fl_Button(15, y, w, h, S("OK"));
9134  p->callback(ok_cb, (void*) this);
9135  p->shortcut(FL_Enter);
9136  p->clear_visible_focus();
9137  new Fl_Box(400 - bw, gy, bw, gh);
9138  p = new Fl_Button(400 - bw + 15, y, w, h, S("Cancel"));
9139  p->callback(cancel_cb, (void*) this);
9140  p->shortcut(FL_Escape);
9141  p->clear_visible_focus();
9142  // Resizable space between buttons
9143  bp = new Fl_Box(bw, gy, 400 - (2 * bw), gh);
9144  //bp->box(FL_DOWN_BOX);
9145  //bp->color(FL_YELLOW);
9146  }
9147  buttonGroup->end();
9148  buttonGroup->resizable(bp);
9149  }
9150  gp->end();
9151  gp->resizable(searchGroup);
9152  resizable(gp);
9153  // Set minimum window size
9154  size_range(400, 165, 0, 0);
9155 #if !CFG_DB_DISABLE
9156  Fl::visual(FL_DOUBLE | FL_INDEX);
9157 #endif // CFG_DB_DISABLE
9158  show();
9159 }
9160 
9161 
9162 // =============================================================================
9163 // Search window destructor
9164 
9165 SearchWindow::~SearchWindow(void)
9166 {
9167 }
9168 
9169 
9170 // =============================================================================
9171 // MIME content element decoder
9172 //
9173 // All the headerfields currently used are mandatory and the CORE module ensures
9174 // that strings are not NULL.
9175 // This must be checked here if optional header fields are used in the future!
9176 //
9177 // The caller is responsible to enc_free() the memory allocated for the result.
9178 
9179 const char* MIMEContent::createMessageHeader(const char* message,
9180  std::size_t len)
9181 {
9182  const char* res = NULL;
9183  core_article_header* hdr = NULL;
9184  std::size_t unused;
9185  Fl_Text_Buffer tb;
9186  char date[20];
9187  std::size_t i = 0;
9188  const char* p;
9189 
9190  // Split and parse header of message
9191  p = core_entity_parser(message, len, &hdr, &unused);
9192  if(NULL == p)
9193  {
9194  // Invalid message
9195  tb.append("[Invalid MIME message/rfc822 content]");
9196  tb.append("\n");
9197  }
9198  else
9199  {
9200  // From header field
9201  tb.append(S("From"));
9202  tb.append(": ");
9203  tb.append(hdr->from);
9204  tb.append("\n");
9205 
9206  // Subject header field
9207  tb.append(S("Subject"));
9208  tb.append(": ");
9209  tb.append(hdr->subject);
9210  tb.append("\n");
9211 
9212  // Date header field (a zero timestamp is treated as missing header field)
9213  tb.append(S("Date"));
9214  tb.append(": ");
9215  if(!hdr->date || enc_convert_posix_to_iso8601(date, hdr->date))
9216  {
9217  // No NLS support here to stay consistent with CORE
9218  tb.append("[Missing or invalid header field]");
9219  }
9220  else
9221  {
9222  tb.append(date);
9223  }
9224  tb.append("\n");
9225 
9226  // Message-ID header field
9227  tb.append(S("Message-ID"));
9228  tb.append(": ");
9229  if(!hdr->msgid[0])
9230  {
9231  // No NLS support here to stay consistent with CORE
9232  tb.append("[Missing or invalid header field]");
9233  }
9234  else
9235  {
9236  tb.append(hdr->msgid);
9237  }
9238  tb.append("\n");
9239 
9240  // Newsgroups header field
9241  tb.append(S("Newsgroups"));
9242  tb.append(": ");
9243  while(hdr->groups[i])
9244  {
9245  if(!i && !hdr->groups[0][0])
9246  {
9247  // No NLS support here to stay consistent with CORE
9248  tb.append("[Missing or invalid header field]");
9249  break;
9250  }
9251  else
9252  {
9253  if(i) { tb.append(","); }
9254  tb.append(hdr->groups[i++]);
9255  }
9256  }
9257  tb.append("\n");
9258  }
9259 
9260  // Copy result
9261  p = tb.text();
9263 
9264  std::free((void*) p);
9266 
9267  return(res);
9268 }
9269 
9270 
9271 // =============================================================================
9272 // MIME content element decoder
9273 //
9274 // \attention
9275 // This method must be reentrant (calls itself to recursively decode nested
9276 // multipart entities
9277 
9278 MIMEContent::MIMEContentListElement*
9279 MIMEContent::decodeElement(const char* p, std::size_t len, char* boundary,
9280  bool alt, bool digest, std::size_t* count)
9281 {
9282  MIMEContentListElement* res = NULL; // Result list
9283  MIMEContentListElement* cle = NULL; // Current list element(s)
9284  MIMEContentListElement* ble = NULL; // Buffered list element(s)
9285  // for multipart/alternative
9286  MIMEContentListElement* mle = NULL; // Encapsulated List element(s)
9287  // for message/rfc822
9288  MIMEContentListElement* lle = NULL; // Last list element
9289  std::size_t ae; // Additional elements
9290  std::size_t bae = 0; // Buffered additional elements
9291  std::size_t num = 0;
9292  enc_mime_mpe* mpe = NULL; // Multipart entities
9293  enc_mime_cte cte;
9294  enc_mime_ct ct;
9295  bool nested_multi;
9296  char nested_boundary[ENC_BO_BUFLEN];
9297  bool nested_alt;
9298  bool nested_digest;
9299  bool message_rfc822;
9300  core_article_header* e_h = NULL;
9301  std::size_t e_len;
9302  const char* q;
9303  std::size_t i;
9304  MIMEContentListElement* tmp;
9305 
9306  // Split multipart message into entities
9307  if(main_debug)
9308  {
9309  if(!std::strlen(boundary))
9310  {
9311  printf("%s: %svvv Extract MIME encapsulated message vvv\n",
9312  CFG_NAME, MAIN_ERR_PREFIX);
9313  }
9314  else
9315  {
9316  printf("%s: %s--- Split MIME multipart entity ---\n",
9317  CFG_NAME, MAIN_ERR_PREFIX);
9318  }
9319  printf("%s: %sSize of body: %lu octets\n",
9320  CFG_NAME, MAIN_ERR_PREFIX, (unsigned long int) len);
9321  }
9322  if(!std::strlen(boundary))
9323  {
9324  num = enc_mime_message(p, len, &mpe);
9325  }
9326  else
9327  {
9328  num = enc_mime_multipart(p, boundary, &mpe);
9329  }
9330  for(i = 0; i < num; ++i)
9331  {
9332  cte = ENC_CTE_BIN;
9333  nested_multi = false;
9334  nested_alt = false;
9335  nested_digest = false;
9336  message_rfc822 = false;
9337  ae = 0;
9338  // Split and parse header of entity
9339  q = core_entity_parser(mpe[i].start, mpe[i].len, &e_h, &e_len);
9340  if(NULL == q)
9341  {
9342  // Invalid entity => Handle whole entity as raw binary octet stream
9343  PRINT_ERROR("Invalid entity in MIME multipart entity");
9344  e_h = NULL;
9345  e_len = mpe[i].len;
9346  ct.type = ENC_CT_APPLICATION;
9348  ct.charset = ENC_CS_UNKNOWN;
9349  ct.flags = 0;
9350  }
9351  else
9352  {
9353  p = q;
9354  // Default to "text/plain; charset=US-ASCII" as defined by RFC 2046
9355  ct.type = ENC_CT_TEXT;
9356  ct.subtype = ENC_CTS_PLAIN;
9357  ct.charset = ENC_CS_ASCII;
9358  ct.flags = 0;
9359  // Use "message/rfc822" for multipart/digest as defined by RFC 2046
9360  if(digest)
9361  {
9362  ct.type = ENC_CT_MESSAGE;
9363  ct.subtype = ENC_CTS_RFC822;
9364  ct.charset = ENC_CS_UNKNOWN;
9365  ct.flags = 0;
9366  message_rfc822 = true;
9367  }
9368  // Check whether content type header field exist
9369  if(NULL == e_h->mime_ct)
9370  {
9371  if(main_debug)
9372  {
9373  if(!digest)
9374  {
9375  printf("%s: %sContent-Type: Text (using default)\n",
9376  CFG_NAME, MAIN_ERR_PREFIX);
9377  }
9378  else
9379  {
9380  printf("%s: %sContent-Type: Message (using default)\n",
9381  CFG_NAME, MAIN_ERR_PREFIX);
9382  }
9383  }
9384  }
9385  else
9386  {
9387  message_rfc822 = false;
9388  // Yes => Check whether content transfer encoding is supported
9389  cte = enc_mime_get_cte(e_h->mime_cte);
9390  if(ENC_CTE_UNKNOWN == cte)
9391  {
9392  PRINT_ERROR("Unknown MIME Content-Transfer-Encoding");
9393  // No => Treat as arbitrary binary data
9394  cte = ENC_CTE_BIN;
9395  // RFC 2049 requires that such content must be treated as type
9396  // "application/octet-stream".
9397  ct.type = ENC_CT_APPLICATION;
9399  ct.charset = ENC_CS_UNKNOWN;
9400  }
9401  else
9402  {
9403  // Yes => Check content type
9404  enc_mime_get_ct(&ct, e_h->mime_ct, nested_boundary);
9405  switch(ct.type)
9406  {
9407  case ENC_CT_TEXT:
9408  {
9409  if(main_debug)
9410  {
9411  printf("%s: %sContent-Type: Text\n",
9412  CFG_NAME, MAIN_ERR_PREFIX);
9413  }
9414  // Check character set
9415  if(ENC_CS_UNKNOWN == ct.charset)
9416  {
9417  // Content character set not supported
9418  // RFC 2049 requires that such content must be treated
9419  // as type "application/octet-stream".
9420  ct.type = ENC_CT_APPLICATION;
9422  ct.charset = ENC_CS_UNKNOWN;
9423  }
9424  break;
9425  }
9426  case ENC_CT_IMAGE:
9427  case ENC_CT_AUDIO:
9428  case ENC_CT_VIDEO:
9429  {
9430  if(main_debug)
9431  {
9432  printf("%s: %sContent-Type: Image, Audio or Video\n",
9433  CFG_NAME, MAIN_ERR_PREFIX);
9434  }
9435  ct.subtype = ENC_CTS_UNKNOWN;
9436  break;
9437  }
9438  case ENC_CT_MULTIPART:
9439  {
9440  if(main_debug)
9441  {
9442  printf("%s: %sContent-Type: Multipart (nested)\n",
9443  CFG_NAME, MAIN_ERR_PREFIX);
9444  }
9445  nested_multi = true;
9446  if(ENC_CTS_ALTERNATIVE == ct.subtype)
9447  {
9448  nested_alt = true;
9449  }
9450  else if(ENC_CTS_DIGEST == ct.subtype)
9451  {
9452  nested_digest = true;
9453  }
9454  break;
9455  }
9456  case ENC_CT_MESSAGE:
9457  {
9458  if(main_debug)
9459  {
9460  printf("%s: %sContent-Type: Message\n",
9461  CFG_NAME, MAIN_ERR_PREFIX);
9462  }
9463  if(ENC_CTS_RFC822 == ct.subtype)
9464  {
9465  message_rfc822 = true;
9466  }
9467  else
9468  {
9469  ct.type = ENC_CT_APPLICATION;
9471  ct.charset = ENC_CS_UNKNOWN;
9472  }
9473  break;
9474  }
9475  default:
9476  {
9477  if(main_debug)
9478  {
9479  printf("%s: %sContent-Type not supported\n",
9480  CFG_NAME, MAIN_ERR_PREFIX);
9481  }
9482  // Handle anything unknown as type
9483  // "application/octet-stream"
9484  ct.type = ENC_CT_APPLICATION;
9486  ct.charset = ENC_CS_UNKNOWN;
9487  break;
9488  }
9489  }
9490  }
9491  }
9492  }
9493  // Special handling for MIME content type "multipart/alternative"
9494  if(alt)
9495  {
9496  // Check whether more sophisticated alternative is supported
9497  if(i)
9498  {
9499  if(!(ENC_CT_TEXT == ct.type && ENC_CTS_PLAIN == ct.subtype
9500  && ENC_CTE_UNKNOWN != cte))
9501  {
9502  // Ignore this alternative
9504  continue;
9505  }
9506  }
9507  }
9508  // Create new content element(s)
9509  if(nested_multi)
9510  {
9511  // Recursively decode nested multipart entities
9512  cle = decodeElement(p, e_len,
9513  nested_boundary, nested_alt, nested_digest, &ae);
9514  }
9515  // Create new element for encapsulated header of type message/rfc822
9516  else if(message_rfc822)
9517  {
9518  // Decode encapsulated entity
9519  nested_boundary[0] = 0;
9520  mle = decodeElement(p, e_len,
9521  nested_boundary, nested_alt, nested_digest, &ae);
9522  // Create additional list element for header of encapsulated message
9523  cte = ENC_CTE_8BIT;
9524  ct.type = ENC_CT_TEXT;
9525  ct.subtype = ENC_CTS_PLAIN;
9526  ct.charset = ENC_CS_UTF_8;
9527  ct.flags = 0;
9528  q = createMessageHeader(p, len);
9529  cle = initElement(q, std::strlen(q), cte, &ct, e_h);
9530  enc_free((void*) q);
9531  ++ae;
9532  // Append element for the message header first
9533  cle->next = mle;
9534  // Append additional list element for end of encapsulated message
9535  SC("Control characters for line break at the end must stay in place")
9536  q = S("End of encapsulated message.\r\n");
9537  mle = initElement(q, std::strlen(q), cte, &ct, e_h);
9538  ++ae;
9539  // Append element for EOM indication at the end
9540  tmp = cle;
9541  while(NULL != tmp->next) { tmp = tmp->next; }
9542  tmp->next = mle;
9543  }
9544  else
9545  {
9546  // Create new element
9547  cle = initElement(p, e_len, cte, &ct, e_h);
9548  ++ae;
9549  }
9550  if(NULL == cle)
9551  {
9552  PRINT_ERROR("Decoding MIME multipart message failed");
9553  break;
9554  }
9555  // Special handling for MIME content type "multipart/alternative"
9556  if(alt)
9557  {
9558  // Update current choice
9559  ble = cle;
9560  bae = ae;
9561  }
9562  else
9563  {
9564  // Append new element(s) to linked list
9565  if(!i) { res = cle; } else { lle->next = cle; }
9566  // Skip to last element
9567  lle = cle;
9568  while(NULL != lle->next) { lle = lle->next; }
9569  // Update element count
9570  *count += ae;
9571  }
9572  // End of loop
9574  }
9575  if(num) { enc_free((void*) mpe); }
9576  // Special handling for MIME content type "multipart/alternative"
9577  if(alt)
9578  {
9579  // Append only the last supported entity to linked list
9580  res = ble;
9581  // Update element count
9582  *count += bae;
9583  }
9584  // Debug message when multipart content or encapsulated message ends
9585  if(main_debug)
9586  {
9587  if(std::strlen(boundary))
9588  {
9589  printf("%s: %s--- End of MIME multipart entity ---\n",
9590  CFG_NAME, MAIN_ERR_PREFIX);
9591  }
9592  else
9593  {
9594  printf("%s: %s^^^ End of MIME encapsulated message ^^^\n",
9595  CFG_NAME, MAIN_ERR_PREFIX);
9596  }
9597  }
9598 
9599  return(res);
9600 }
9601 
9602 
9603 // =============================================================================
9604 // MIME content element constructor
9605 
9606 MIMEContent::MIMEContentListElement*
9607 MIMEContent::initElement(const char* body, std::size_t body_len,
9608  enc_mime_cte cte, enc_mime_ct* ct,
9609  struct core_article_header* hdr)
9610 {
9611  MIMEContentListElement* cle = new MIMEContentListElement;
9612  char* cp;
9613  Fl_Text_Buffer tb;
9614  bool headerPresent = false;
9615 
9616  // Content-Transfer-Encoding
9617  cle->cte = cte;
9618 
9619  // Content-Type
9620  std::memcpy((void*) &cle->ct, (void*) ct, sizeof(enc_mime_ct));
9621 
9622  // Body
9623  cp = new char[body_len + (std::size_t) 1];
9624  std::strncpy(cp, body, body_len); cp[body_len] = 0;
9625  cle->content = cp;
9626 
9627  // Entity headers for multipart content
9628  cle->type = ENC_CD_INLINE;
9629  cle->filename = NULL;
9630  cle->header = NULL;
9631  tb.text("");
9632  if(NULL != hdr && NULL != hdr->mime_cte)
9633  {
9634  tb.append(S("Transfer-Encoding"));
9635  tb.append(": ");
9636  tb.append(hdr->mime_cte);
9637  tb.append("\n");
9638  headerPresent = true;
9639  }
9640  if(NULL != hdr && NULL != hdr->mime_ct)
9641  {
9642  tb.append(S("Content-Type"));
9643  tb.append(": ");
9644  tb.append(hdr->mime_ct);
9645  tb.append("\n");
9646  headerPresent = true;
9647  }
9648  if(NULL != hdr && NULL != hdr->mime_cd)
9649  {
9650  tb.append(S("Content-Disposition"));
9651  tb.append(": ");
9652  tb.append(hdr->mime_cd);
9653  tb.append("\n");
9654  headerPresent = true;
9655  // Store type and filename for entity
9656  enc_mime_get_cd(hdr->mime_cd, &cle->type, &cle->filename);
9657  }
9658  if(headerPresent)
9659  {
9660  cle->header = tb.text();
9661  }
9662 
9663  // Terminate linked list
9664  cle->next = NULL;
9665 
9666  return(cle);
9667 }
9668 
9669 
9670 // =============================================================================
9671 // MIME content object constructor
9672 
9673 MIMEContent::MIMEContent(struct core_article_header* h, const char* p)
9674 {
9675  char boundary[ENC_BO_BUFLEN];
9676  enc_mime_cte cte = ENC_CTE_BIN;
9677  enc_mime_ct ct;
9678  bool multi;
9679  bool alt; // Multipart entity contains multiple variants of same content
9680  bool digest; // Default content type for multipart is message/rfc822
9681 
9682  // Init linked list
9683  partList = NULL;
9684  partNum = 0;
9685 
9686  // Check for MIME declaration (always assume version 1.0)
9687  if(NULL == h->mime_v)
9688  {
9689  if(NULL != h->mime_cte || NULL != h->mime_ct)
9690  {
9691  PRINT_ERROR("Trying to decode MIME article "
9692  "with missing MIME-Version header field");
9693  }
9694  }
9695  // Default to "text/plain; charset=US-ASCII" as defined by RFC 2045
9696  multi = false;
9697  alt = false;
9698  digest = false;
9699  ct.type = ENC_CT_TEXT;
9700  ct.subtype = ENC_CTS_PLAIN;
9701  ct.charset = ENC_CS_ASCII;
9702  ct.flags = 0;
9703  // Check whether content type header field exist
9704  if(NULL != h->mime_ct || NULL != h->mime_cte)
9705  {
9706  // Yes => Check whether content transfer encoding is supported
9707  cte = enc_mime_get_cte(h->mime_cte);
9708  if(ENC_CTE_UNKNOWN == cte)
9709  {
9710  // No => Treat as arbitrary binary data
9711  cte = ENC_CTE_BIN;
9712  // RFC 2049 requires that such content must be treated as type
9713  // "application/octet-stream".
9714  ct.type = ENC_CT_APPLICATION;
9716  ct.charset = ENC_CS_UNKNOWN;
9717  }
9718  else
9719  {
9720  // Yes => Check content type
9721  enc_mime_get_ct(&ct, h->mime_ct, boundary);
9722  switch(ct.type)
9723  {
9724  case ENC_CT_TEXT:
9725  {
9726  // Check character set
9727  if(ENC_CS_UNKNOWN == ct.charset)
9728  {
9729  // Content character set not supported
9730  // RFC 2049 requires that such content must be treated as
9731  // type "application/octet-stream".
9732  ct.type = ENC_CT_APPLICATION;
9734  ct.charset = ENC_CS_UNKNOWN;
9735  }
9736  break;
9737  }
9738  case ENC_CT_IMAGE:
9739  case ENC_CT_AUDIO:
9740  case ENC_CT_VIDEO:
9741  {
9742  break;
9743  }
9744  case ENC_CT_MULTIPART:
9745  {
9746  // The boundary delimiter is already stored in 'boundary'
9747  // Set flag and split the content later
9748  multi = true;
9749  // Set flag if subtype is "alternative"
9750  if(ENC_CTS_ALTERNATIVE == ct.subtype) { alt = true; }
9751  // Set flag if subtype is "digest"
9752  else if(ENC_CTS_DIGEST == ct.subtype) { digest = true; }
9753  break;
9754  }
9755  default:
9756  {
9757  // Handle anything unknown as type "application/octet-stream"
9758  ct.type = ENC_CT_APPLICATION;
9760  ct.charset = ENC_CS_UNKNOWN;
9761  break;
9762  }
9763  }
9764  }
9765  }
9766 
9767  // Create linked list of entities
9768  if(!multi)
9769  {
9770  multipart = false;
9771  partList = initElement(p, std::strlen(p), cte, &ct, h);
9772  partNum = 1;
9773  }
9774  else
9775  {
9776  multipart = true;
9777  // Recursively create elements from multipart entities
9778  partList = decodeElement(p, std::strlen(p),
9779  boundary, alt, digest, &partNum);
9780  }
9781 }
9782 
9783 
9784 // =============================================================================
9785 // MIME content object constructor
9786 
9787 MIMEContent::~MIMEContent(void)
9788 {
9789  MIMEContentListElement* cle = partList;
9790  MIMEContentListElement* nle;
9791 
9792  while(NULL != cle)
9793  {
9794  nle = cle->next;
9795  enc_free((void*) cle->filename);
9796  std::free((void*) cle->header);
9797  delete[] cle->content;
9798  delete cle;
9799  cle = nle;
9800  }
9801 }
9802 
9803 
9804 // =============================================================================
9805 // Subscribe tree OK button callback
9806 
9807 void SubscribeWindow::ok_cb_i(void)
9808 {
9809  Fl_Tree_Item* i = subscribeTree->first_selected_item();
9810  int rv = -1;
9811  std::size_t ii;
9812  char buf[1024];
9813  char* name;
9814 
9815  // Hide root node tree to remove it from pathname
9816  subscribeTree->showroot(0);
9817 
9818  // Update subscribed groups
9819  mainWindow->groupStateExport();
9820  while(NULL != i)
9821  {
9822  // Check whether an menu style path was stored in user data
9823  if('\0' != ((const char*) i->user_data())[0])
9824  {
9825  // Yes => Use it
9826  name = (char*) i->user_data();
9827  rv = 0;
9828  }
9829  else
9830  {
9831  // No => Get menu style path from widget
9832  rv = subscribeTree->item_pathname(buf, 1024, i);
9833  name = buf;
9834  }
9835  if(rv)
9836  {
9837  if(-2 == rv) { PRINT_ERROR("Group name too long and ignored"); }
9838  else { PRINT_ERROR("Group not found (bug)"); }
9839  }
9840  else
9841  {
9842  ii = 0;
9843  do
9844  { if('/' == name[ii]) { name[ii] = '.'; } }
9845  while(name[ii++]);
9846  // Because the number of selected groups is usually low, we can add
9847  // them one after the other without relevant performance impact
9848  rv = core_subscribe_group(name);
9849  if(rv)
9850  {
9851  PRINT_ERROR("Updating list of subscribed groups failed");
9852  break;
9853  }
9854  }
9855  i = subscribeTree->next_selected_item(i);
9856  rv = 0;
9857  }
9858 
9859  // Check result
9860  if(rv)
9861  {
9862  SC("Do not use non-ASCII for the translation of this item")
9863  fl_message_title(S("Error"));
9864  fl_alert("%s", S("Error while updating subscribed group database"));
9865  }
9866  else { UI_STATUS(S("Subscribed groups stored.")); }
9867 
9868  // This check should silcene code checking tools
9869  // ('mainWindow' is always present)
9870  if(NULL != mainWindow)
9871  {
9872  // Import new group list
9873  mainWindow->groupListImport();
9874  }
9875 
9876  // Schedule destruction of subscribe window
9877  Fl::delete_widget(this);
9878 }
9879 
9880 
9881 // =============================================================================
9882 // Subscribe window constructor
9883 
9884 SubscribeWindow::SubscribeWindow(const char* label, core_groupdesc* glist,
9885  core_grouplabel* labels) :
9886  UI_WINDOW_CLASS(730, 350, label), grouplist(glist), grouplabels(labels)
9887 {
9888  Fl_Group* gp;
9889  Fl_Box* bp;
9890  Fl_Button* p;
9891  int y, w, h, gy, gh, bw;
9892  int w1, w2, h1, h2;
9893 
9894  // Configure subscribe window
9895  copy_label(label);
9896  callback(cancel_cb, (void*) this);
9897  set_modal();
9898 
9899  // Add widgets --------------------------------------------------------------
9900  begin();
9901 
9902  // Create widget group
9903  subscribeGroup = new Fl_Group(0, 0, 730, 350);
9904  subscribeGroup->begin();
9905  {
9906  // Calculate required widths and heights
9907  gui_set_default_font();
9908  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
9909  fl_measure(S("OK"), w2 = 0, h2 = 0);
9910  if(w1 > w2) { w = w1; } else { w = w2; }
9911  if(h1 > h2) { h = h1; } else { h = h2; }
9912  w += 30;
9913  h += 10;
9914  gh = h + 10;
9915  gy = 350 - gh;
9916  // Add newsgroup tree
9917  subscribeTree = new Fl_Tree(0, 0, 730, 350 - gh);
9918  // Explicitly set foreground color to FLTK default
9919  // (default is not the same as for the other widgets)
9920  subscribeTree->item_labelfgcolor(FL_FOREGROUND_COLOR);
9921  // Special handling for the root item that was already present
9922  subscribeTree->root()->labelfgcolor(FL_FOREGROUND_COLOR);
9923  subscribeTree->root_label("Usenet");
9924  subscribeTree->margintop(5);
9925  subscribeTree->marginleft(0);
9926  subscribeTree->openchild_marginbottom(5);
9927  subscribeTree->selectmode(FL_TREE_SELECT_MULTI);
9928  subscribeTree->sortorder(FL_TREE_SORT_ASCENDING);
9929  subscribeTree->callback(tree_cb, (void*) this);
9930  // Add button bar at bottom
9931  gp = new Fl_Group(0, gy, 730, gh);
9932  gp->begin();
9933  {
9934  bw = w + 30;
9935  y = gy + 5;
9936  new Fl_Box(0, gy, bw, gh);
9937  p = new Fl_Button(15, y, w, h, S("OK"));
9938  p->callback(ok_cb, (void*) this);
9939  new Fl_Box(730 - bw, gy, bw, gh);
9940  p = new Fl_Button(730 - bw + 15, y, w, h, S("Cancel"));
9941  p->callback(cancel_cb, (void*) this);
9942  // Resizable space between buttons
9943  bp = new Fl_Box(bw, gy, 730 - (2 * bw), gh);
9944  }
9945  gp->end();
9946  gp->resizable(bp);
9947  }
9948  subscribeGroup->end();
9949  subscribeGroup->resizable(subscribeTree);
9950  resizable(subscribeGroup);
9951 
9952  end();
9953  // --------------------------------------------------------------------------
9954 
9955  // Set minimum window size
9956  size_range(2 * bw, 100, 0, 0);
9957 #if !CFG_DB_DISABLE
9958  Fl::visual(FL_DOUBLE | FL_INDEX);
9959 #endif // CFG_DB_DISABLE
9960  show();
9961 }
9962 
9963 
9964 // =============================================================================
9965 // Subscribe window destructor
9966 
9967 SubscribeWindow::~SubscribeWindow(void)
9968 {
9969  core_free((void*) grouplabels);
9970  core_free((void*) grouplist);
9971 }
9972 
9973 
9974 // =============================================================================
9975 // Protocol console update method
9976 
9977 void ProtocolConsole::update(void)
9978 {
9979  int rv;
9980  const char* logname;
9981  char buffer[128];
9982  unsigned int n;
9983 
9984  // Update protocol console
9985  if(!nolog)
9986  {
9987  if(!logfp)
9988  {
9989  logname = log_get_logpathname();
9990  if(logname)
9991  {
9992  logfp = fopen(logname, "r");
9993  log_free((void*) logname); logname = NULL;
9994  if(!logfp)
9995  {
9996  nolog = 1;
9997  protocolConsole->consoleDisplay->insert(
9998  S("Logfile is only available in debug mode.") );
9999  protocolConsole->consoleDisplay->insert("\n");
10000  protocolConsole->consoleDisplay->insert(
10001  S("Use command line option -debug for debug mode.") );
10002  }
10003  }
10004  }
10005  else
10006  {
10007  while(!feof(logfp))
10008  {
10009  for(n = 0; n < sizeof(buffer) - 1; ++n)
10010  {
10011  rv = fgetc(logfp);
10012  if(EOF == rv) { break; }
10013  else
10014  {
10015  // Replace CR characters with spaces
10016  if(0x0D == rv) { buffer[n] = 0x20; }
10017  else { buffer[n] = (char) (unsigned char) rv; }
10018  }
10019  }
10020  buffer[n] = 0;
10021  protocolConsole->consoleDisplay->insert(buffer);
10022  // Drive GUI
10023  Fl::check();
10024  // The protocol console was possibly destroyed by a callback
10025  // Verify that it's still present
10026  if(NULL == protocolConsole) { break; }
10027  }
10028  if(protocolConsole) { clearerr(logfp); }
10029  }
10030  }
10031 }
10032 
10033 
10034 // =============================================================================
10035 // Protocol console constructor
10036 
10037 ProtocolConsole::ProtocolConsole(const char* label) :
10038  UI_WINDOW_CLASS(730, 395, label), logfp(NULL), nolog(0)
10039 {
10040  copy_label(label);
10041  consoleText = new Fl_Text_Buffer();
10042  consoleDisplay = new Fl_Text_Display(0, 0, 730, 395);
10043  consoleDisplay->buffer(consoleText);
10044  resizable(consoleDisplay);
10045  callback(exit_cb, (void*) this);
10046 #if !CFG_DB_DISABLE
10047  Fl::visual(FL_DOUBLE | FL_INDEX);
10048 #endif // CFG_DB_DISABLE
10049  show();
10050 }
10051 
10052 
10053 // =============================================================================
10054 // Protocol console destructor
10055 
10056 ProtocolConsole::~ProtocolConsole(void)
10057 {
10058  if(logfp) { fclose(logfp); }
10059 
10060  // Release memory
10061  delete consoleDisplay;
10062  delete consoleText;
10063 
10064  protocolConsole = NULL;
10065 }
10066 
10067 
10068 // =============================================================================
10069 // Message-ID search window callback
10070 
10071 void MIDSearchWindow::ok_cb_i(void)
10072 {
10073  std::size_t limit = 248; // Octets (without angle brackets)
10074  const char* message_id = mid->value();
10075  char* buf = NULL;
10076  char* p;
10077  std::size_t len;
10078 
10079  // Copy Message-ID
10080  len = std::strlen(message_id);
10081  buf = new char[len + (std::size_t) 1];
10082  std::strcpy(buf, message_id);
10083 
10084  // Remove potential surrounding whitespace and angle brackets
10085  p = buf;
10086  while(0x09 == (int) p[0] || ' ' == p[0] || '<' == p[0])
10087  {
10088  p = &p[1];
10089  --len;
10090  }
10091  while(0x09 == (int) p[len - (std::size_t) 1]
10092  || ' ' == p[len - (std::size_t) 1] || '>' == p[len - (std::size_t) 1])
10093  {
10094  p[len - (std::size_t) 1] = 0;
10095  --len;
10096  }
10097 
10098  // Check length limit
10099  if(limit < len)
10100  {
10101  SC("Do not use non-ASCII for the translation of this item")
10102  fl_message_title(S("Error"));
10103  fl_alert("%s",
10104  S("Message-ID too long\nLimit is 250 characters, including angle brackets"));
10105  }
10106 
10107  // Destroy configuration window
10108  Fl::delete_widget(this);
10109 
10110  // Search article (and display it in separate window if found)
10111  mainWindow->viewArticle(UI_CB_START, (const char*) p);
10112  delete[] buf;
10113 }
10114 
10115 
10116 // =============================================================================
10117 // Message-ID search window constructor
10118 
10119 MIDSearchWindow::MIDSearchWindow(const char* label) :
10120  UI_WINDOW_CLASS(700, 150, label)
10121 {
10122  Fl_Group* gp1;
10123  Fl_Group* gp2;
10124  Fl_Box* bp;
10125  Fl_Button* p;
10126  int y, w, h, gy, gh, bw;
10127  int w1, w2, h1, h2;
10128 
10129  // Configure Message-ID search window
10130  copy_label(label);
10131  set_modal();
10132  callback(cancel_cb, (void*) this);
10133 
10134  // Create widget group
10135  cfgGroup = new Fl_Group(0, 0, 700, 150);
10136  cfgGroup->begin();
10137  {
10138  // Calculate required widths and heights
10139  gui_set_default_font();
10140  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
10141  fl_measure(S("OK"), w2 = 0, h2 = 0);
10142  if(w1 > w2) { w = w1; } else { w = w2; }
10143  if(h1 > h2) { h = h1; } else { h = h2; }
10144  w += 30;
10145  h += 10;
10146  gh = h + 10;
10147  gy = 150 - gh;
10148 
10149  gp1 = new Fl_Group(0, 0, 700, gy);
10150  gp1->begin();
10151  {
10152  // Add "From" Name field
10153  mid = new Fl_Input(15, 35, 670, 30, "Message-ID:");
10154  mid->align(FL_ALIGN_TOP_LEFT);
10155  // Resizable space below input fields
10156  bp = new Fl_Box(0, 100, 700, gy - 100);
10157  //bp->box(FL_DOWN_BOX);
10158  }
10159  gp1->end();
10160  gp1->resizable(bp);
10161  // Add button bar at bottom
10162  gp2 = new Fl_Group(0, gy, 700, gh);
10163  gp2->begin();
10164  {
10165  bw = w + 30;
10166  y = gy + 5;
10167  new Fl_Box(0, gy, bw, gh);
10168  p = new Fl_Return_Button(15, y, w, h, S("OK"));
10169  p->callback(ok_cb, (void*) this);
10170  new Fl_Box(500 - bw, gy, bw, gh);
10171  p = new Fl_Button(700 - bw + 15, y, w, h, S("Cancel"));
10172  p->callback(cancel_cb, (void*) this);
10173  p->shortcut(FL_Escape);
10174 
10175  // Resizable space between buttons
10176  bp = new Fl_Box(bw, gy, 700 - (2 * bw), gh);
10177  }
10178  gp2->end();
10179  gp2->resizable(bp);
10180  }
10181  cfgGroup->end();
10182  cfgGroup->resizable(gp1);
10183  resizable(cfgGroup);
10184  // Set minimum window size
10185  size_range(700, 100 + gh, 0, 0);
10186 #if !CFG_DB_DISABLE
10187  Fl::visual(FL_DOUBLE | FL_INDEX);
10188 #endif // CFG_DB_DISABLE
10189  show();
10190 }
10191 
10192 
10193 // =============================================================================
10194 // Message-ID search window destructor
10195 
10196 MIDSearchWindow::~MIDSearchWindow(void)
10197 {
10198 }
10199 
10200 
10201 // =============================================================================
10202 // Bug report window constructor
10203 
10204 BugreportWindow::BugreportWindow(const char* label, const char* content) :
10205  UI_WINDOW_CLASS(795, 395, label)
10206 {
10207  std::size_t len;
10208 
10209  copy_label(label);
10210  len = std::strlen(content);
10211  if(INT_MAX < len) { len = 0; }
10212  bugreportText = new Fl_Text_Buffer((int) ++len, 0);
10213  bugreportText->insert(0, content);
10214  bugreportDisplay = new Fl_Text_Display(0, 0, 795, 395);
10215  bugreportDisplay->textfont(FL_COURIER);
10216  bugreportDisplay->buffer(bugreportText);
10217  resizable(bugreportDisplay);
10218  callback(exit_cb, (void*) this);
10219 #if !CFG_DB_DISABLE
10220  Fl::visual(FL_DOUBLE | FL_INDEX);
10221 #endif // CFG_DB_DISABLE
10222  show();
10223 }
10224 
10225 
10226 // =============================================================================
10227 // Bug report window destructor
10228 
10229 BugreportWindow::~BugreportWindow(void)
10230 {
10231  // Release memory
10232  delete bugreportDisplay;
10233  delete bugreportText;
10234 }
10235 
10236 
10237 // =============================================================================
10238 // Message of the day window constructor
10239 
10240 MotdWindow::MotdWindow(const char* motd, const char* label) :
10241  UI_WINDOW_CLASS(795, 395, label)
10242 {
10243  copy_label(label);
10244  motdText = new Fl_Text_Buffer();
10245  motdText->text(motd);
10246  motdDisplay = new Fl_Text_Display(0, 0, 795, 395);
10247  motdDisplay->textfont(FL_COURIER);
10248  motdDisplay->buffer(motdText);
10249  resizable(motdDisplay);
10250  callback(exit_cb, (void*) this);
10251 #if !CFG_DB_DISABLE
10252  Fl::visual(FL_DOUBLE | FL_INDEX);
10253 #endif // CFG_DB_DISABLE
10254  show();
10255 }
10256 
10257 
10258 // =============================================================================
10259 // Message of the day window destructor
10260 
10261 MotdWindow::~MotdWindow(void)
10262 {
10263  // Release memory
10264  delete motdDisplay;
10265  delete motdText;
10266 }
10267 
10268 
10269 // =============================================================================
10270 // License window constructor
10271 
10272 LicenseWindow::LicenseWindow(const char* label) :
10273  UI_WINDOW_CLASS(795, 395, label)
10274 {
10275  int rv;
10276 
10277  copy_label(label);
10278  licenseText = new Fl_Text_Buffer();
10279  rv = licenseText->loadfile(CFG_LICENSE_PATH "/license.txt");
10280  if(rv) { licenseText->insert(0, S("Error: License file not found")); }
10281  licenseDisplay = new Fl_Text_Display(0, 0, 795, 395);
10282  licenseDisplay->textfont(FL_COURIER);
10283  licenseDisplay->buffer(licenseText);
10284  resizable(licenseDisplay);
10285  callback(exit_cb, (void*) this);
10286 #if !CFG_DB_DISABLE
10287  Fl::visual(FL_DOUBLE | FL_INDEX);
10288 #endif // CFG_DB_DISABLE
10289  show();
10290 }
10291 
10292 
10293 // =============================================================================
10294 // License window destructor
10295 
10296 LicenseWindow::~LicenseWindow(void)
10297 {
10298  // Release memory
10299  delete licenseDisplay;
10300  delete licenseText;
10301 }
10302 
10303 
10304 // =============================================================================
10305 // Article window format and print some header fields of article
10306 
10307 const char* ArticleWindow::printHeaderFields(struct core_article_header* h)
10308 {
10309  return(gui_print_header_fields(h));
10310 }
10311 
10312 
10313 // =============================================================================
10314 // Article window update
10315 
10316 void ArticleWindow::articleUpdate(Fl_Text_Buffer* article)
10317 {
10318  // See RFC 3986 for URI format
10319  const char* url[] = { "http://", "https://", "ftp://", "nntp://",
10320  "news:", "mailto:", NULL };
10321  const std::size_t url_len[] = { 7, 8, 6, 7, 5, 7 };
10322  const char bold = 'A'; // Bold text for header field names
10323  const char sig = 'B'; // Signature starting with "-- " separator
10324  const char cit = 'C'; // External citation ("|" at start of line)
10325  const char link = 'D'; // Hyperlink
10326  const char plain = 'E'; // Normal article text
10327  const char l1 = 'F'; // 1st citation level
10328  const char l2 = 'G'; // 2nd citation level
10329  const char l3 = 'H'; // 3rd citation level
10330  const char l4 = 'I'; // 4th citation level
10331  char* style;
10332  std::size_t len;
10333  std::size_t i;
10334  std::size_t ii = 0; // Index in current line
10335  std::size_t iii = 0;
10336  std::size_t iiii;
10337  std::size_t url_i;
10338  bool sol = true; // Start Of Line flag
10339  char hs = plain;
10340  bool ready = false; // Flag indicating positions beyond header separator
10341  int ss = 0;
10342  bool delim = false; // Hyperlink delimiter
10343  bool signature = false; // Flag indicating signature
10344  bool citation = false; // Flag indicating external citation
10345  bool hyperlink = false; // Flag indicating hyperlink
10346  std::size_t cl = 0;
10347  bool cl_lock = false;
10348  char c;
10349  int pos;
10350 
10351  // Replace current article
10352  if(NULL == article)
10353  {
10354  PRINT_ERROR("Article display request without content ignored (bug)");
10355  return;
10356  }
10357  gui_check_article(article);
10358  articleText = article;
10359  articleDisplay->buffer(articleText);
10360 
10361 #if !UI_AW_REFERENCES
10362  // Remove references line for ArticleWindow (not useful without hyperlinks)
10363  if (1 == articleText->findchar_forward(0, 0x1DU, &pos))
10364  {
10365  articleText->remove(articleText->line_start(pos),
10366  articleText->line_end(pos) + 1);
10367  }
10368 #endif // ! UI_AW_REFERENCES
10369 
10370  // Soft Hyphen (SHY) handling
10371  gui_process_shy(articleText);
10372 
10373  // Create article content style
10374  style = articleText->text();
10375  if(NULL == style) { len = 0; }
10376  else { len = std::strlen(style); }
10377  if(INT_MAX < len) { len = INT_MAX; }
10378  for(i = 0; i < len; ++i)
10379  {
10380  if('\n' == style[i])
10381  {
10382  sol = true;
10383  hyperlink = false;
10384  citation = false;
10385  continue;
10386  }
10387  if(hyperlink)
10388  {
10389  // Check for end of hyperlink
10390  // According to RFC 3986 whitespace, double quotes and angle brackets
10391  // are accepted as delimiters.
10392  // Whitespace is interpreted as SP or HT, Unicode whitespace is not
10393  // accepted as delimiter.
10394  c = style[i];
10395  if(' ' == c || 0x09 == (int) c || '>' == c || '"' == c)
10396  {
10397  hyperlink = false;
10398  }
10399  }
10400  // Highlight external citations (via '|' or '!')
10401  if(sol && ('|' == style[i] || '!' == style[i]))
10402  {
10403  if(!hyperlink) { citation = true; }
10404  }
10405  // Check for start of line
10406  if(sol) { sol = false; delim = true; ss = 0; cl = 0; ii = 0; }
10407  else { ++ii; }
10408  if(signature)
10409  {
10410  // Check for end of signature in potential multipart message
10411  if('|' == style[i])
10412  {
10413  if(79U == ii)
10414  {
10415  for(iiii = i - ii; iiii < i; ++iiii)
10416  {
10417  if('_' != articleText->byte_at((int) iiii)) { break; }
10418  }
10419  if(iiii == i)
10420  {
10421  for(iiii = 0; iiii <= ii; ++iiii)
10422  {
10423  style[i - iiii] = plain;
10424  }
10425  signature = false;
10426  }
10427  }
10428  }
10429  }
10430  if(!ready)
10431  {
10432  // Check for header separator <SOL>"____...____|"
10433  if(!ii)
10434  {
10435  if('_' != style[i]) { hs = bold; } else { hs = plain; }
10436  iii = 0;
10437  }
10438  if('|' == style[i] && 79U <= iii) { ready = true; }
10439  else if('_' == style[i]) { ++iii; }
10440  if(':' == style[i]) { hs = plain; }
10441  style[i] = hs;
10442  }
10443  else
10444  {
10445  // Check for signature separator <SOL>"-- "
10446  if('-' == style[i] && !ii) { ss = 1; }
10447  if('-' == style[i] && 1U == ii && 1 == ss) { ss = 2; }
10448  if(' ' == style[i] && 2U == ii && 2 == ss)
10449  {
10450  // Attention: This EOL check requires POSIX line format!
10451  if((char) 0x0A == style[i + 1U])
10452  {
10453  if(gui_last_sig_separator(&style[i + 1U]))
10454  {
10455  style[i] = sig; style[i - 1U] = sig; style[i - 2U] = sig;
10456  signature = true;
10457  }
10458  }
10459  }
10460  // Check for hyperlink
10461  url_i = 0;
10462  while(NULL != url[url_i])
10463  {
10464  if(!std::strncmp(&style[i], url[url_i], url_len[url_i]))
10465  {
10466  if(delim)
10467  {
10468  style[i] = link;
10469  // ArticleWindow does not support hyperlinks
10470  //hyperlink = true;
10471  }
10472  }
10473  ++url_i;
10474  }
10475  c = style[i];
10476  if(' ' == c || 0x09 == (int) c || '<' == c || '"' == c)
10477  {
10478  delim = true;
10479  }
10480  else { delim = false; }
10481  if(1)
10482  {
10483  // Highlight citation levels of regular content
10484  if(!ii)
10485  {
10486  if('>' == style[i]) { cl_lock = false; }
10487  else { cl_lock = true; }
10488  }
10489  if('>' == style[i] && !cl_lock) { ++cl; }
10490  if('>' != style[i] && ' ' != style[i] && (char) 9 != style[i])
10491  {
10492  cl_lock = true;
10493  }
10494  if(4U < cl) { cl = 1; } // Rotate colors if too many levels
10495  switch(cl)
10496  {
10497  case 1: { style[i] = l1; break; }
10498  case 2: { style[i] = l2; break; }
10499  case 3: { style[i] = l3; break; }
10500  case 4: { style[i] = l4; break; }
10501  default: { style[i] = plain; break; }
10502  }
10503  }
10504  // Override current style for signature, citations and hyperlinks
10505  // (hyperlinks have highest precedence)
10506  if(signature) { style[i] = sig; }
10507  if(citation && !signature) { style[i] = cit; }
10508  if(hyperlink) { style[i] = link; }
10509  }
10510  }
10511  articleStyle = new Fl_Text_Buffer((int) len, 0);
10512  if(NULL != style) { articleStyle->text(style); }
10513  std::free((void*) style);
10514 
10515  // Activate content style
10516  articleDisplay->highlight_data(articleStyle, styles, styles_len, 'A', NULL,
10517  NULL);
10518 
10519  // Replace potential GS marker with SP
10520  if (1 == articleText->findchar_forward(0, 0x1DU, &pos))
10521  {
10522  articleText->replace(pos, pos + 1, " ");
10523  }
10524 }
10525 
10526 
10527 // =============================================================================
10528 // Article window constructor
10529 
10530 ArticleWindow::ArticleWindow(const char* article, const char* label) :
10531  UI_WINDOW_CLASS(795, 395, label)
10532 {
10533  Fl_Color signature = conf_integer_check(CONF_COLOR_SIGNATURE, 0x00, 0xFF);
10534  Fl_Color external = conf_integer_check(CONF_COLOR_EXTERNAL, 0x00, 0xFF);
10535  Fl_Color hyperlink = conf_integer_check(CONF_COLOR_HYPERLINK, 0x00, 0xFF);
10536  Fl_Color level1 = conf_integer_check(CONF_COLOR_LEVEL1, 0x00, 0xFF);
10537  Fl_Color level2 = conf_integer_check(CONF_COLOR_LEVEL2, 0x00, 0xFF);
10538  Fl_Color level3 = conf_integer_check(CONF_COLOR_LEVEL3, 0x00, 0xFF);
10539  Fl_Color level4 = conf_integer_check(CONF_COLOR_LEVEL4, 0x00, 0xFF);
10540  const Fl_Color styles_colors[UI_STYLES_LEN] =
10541  {
10542  FL_FOREGROUND_COLOR, // Plain bold
10543  signature, // Signature
10544  external, // External citation
10545  hyperlink, // Hyperlink
10546  // -----------------------------------------------------------------------
10547  // Keep the following group continuous
10548  FL_FOREGROUND_COLOR, // Content
10549  level1, // Thread citation level 1 ("> ")
10550  level2, // Thread citation level 2 ("> > ")
10551  level3, // Thread citation level 3 ("> > > ")
10552  level4 // Thread citation level 4 ("> > > > ")
10553  };
10554  Fl_Group* gp;
10555  Fl_Box* bp;
10556  Fl_Button* buttonp;
10557  int y, w, h, gy, gh, bw;
10558  int rv = -1;
10559  char* raw;
10560  char* eoh;
10561  const char* q = NULL;
10562  Fl_Text_Buffer* tb;
10563  const char* hdr = NULL;
10564  int ii;
10565 
10566  copy_label(label);
10567 
10568  // Init alternate article hierarchy pointer
10569  alt_hier = NULL;
10570 
10571  // Init text buffer pointers
10572  articleText = NULL;
10573  articleStyle = NULL;
10574  styles = NULL;
10575 
10576  // Init MIME object pointer
10577  mimeData = NULL;
10578 
10579  // Create new article hierarchy
10580  rv = core_hierarchy_manager(&alt_hier, CORE_HIERARCHY_INIT, 0);
10581  if(!rv)
10582  {
10583  // Add article to alternative hierarchy
10584  rv = core_hierarchy_manager(&alt_hier, CORE_HIERARCHY_ADD, 1, article);
10585  }
10586  if(!rv)
10587  {
10588  raw = new char[std::strlen(article) + (std::size_t) 1];
10589  std::strcpy(raw, article);
10590  // Search for end of header
10591  // Attention: Overloaded and both prototypes different than in C!
10592  eoh = std::strstr(raw, "\r\n\r\n");
10593  if(NULL == eoh)
10594  {
10595  // Body not found
10596  PRINT_ERROR("Processing of article failed");
10597  SC("Do not use non-ASCII for the translation of this item")
10598  fl_message_title(S("Error"));
10599  fl_alert("%s", S("Processing of article failed"));
10600  delete[] raw;
10601  }
10602  else
10603  {
10604  // Set pointer to body data
10605  q = &eoh[4];
10606  // Create text buffer
10607  tb = new Fl_Text_Buffer(0, 0);
10608  // Print formatted header data to text buffer
10609  hdr = printHeaderFields(alt_hier->child[0]->header);
10610  if(NULL != hdr)
10611  {
10612  tb->text(hdr);
10613  delete[] hdr;
10614  }
10615  // Print header/body delimiter
10616  tb->append(ENC_DELIMITER);
10617 
10618  // Create MIME object from content
10619  mimeData = new MIMEContent(alt_hier->child[0]->header, q);
10620  delete[] raw;
10621  gui_decode_mime_entities(tb, mimeData,
10622  alt_hier->child[0]->header->msgid);
10623 
10624  // Configure article window
10625  callback(cancel_cb, (void*) this);
10626  set_modal();
10627 
10628  // Add widgets --------------------------------------------------------
10629  begin();
10630 
10631  // Create widget group
10632  articleGroup = new Fl_Group(0, 0, 795, 395);
10633  articleGroup->begin();
10634  {
10635  // Calculate required widths and heights
10636  gui_set_default_font();
10637  fl_measure(S("Cancel"), w = 0, h = 0);
10638  w += 30;
10639  h += 10;
10640  gh = h + 10;
10641  gy = 395 - gh;
10642  // Add text field
10643  articleDisplay = new My_Text_Display(0, 0, 795, 395 - gh);
10644  articleDisplay->textfont(FL_COURIER);
10645  // Allocate and init styles
10646  styles_len = UI_STYLES_LEN;
10647  styles = new Fl_Text_Display::Style_Table_Entry[styles_len];
10648  styles[0].color = FL_FOREGROUND_COLOR;
10649  styles[0].font = FL_COURIER_BOLD;
10650  styles[0].size = articleDisplay->textsize();
10651  styles[0].attr = 0;
10652  for(ii = 1; ii < styles_len; ++ii)
10653  {
10654  styles[ii].color = styles_colors[ii];
10655  styles[ii].font = articleDisplay->textfont();
10656  styles[ii].size = articleDisplay->textsize();
10657  styles[ii].attr = 0;
10658  }
10659  // Set text buffer
10660  articleUpdate(tb);
10661  resizable(articleDisplay);
10662  // Add button bar at bottom
10663  gp = new Fl_Group(0, gy, 795, gh);
10664  gp->begin();
10665  {
10666  bw = w + 30;
10667  y = gy + 5;
10668  new Fl_Box(0, gy, bw, gh);
10669  new Fl_Box(795 - bw, gy, bw, gh);
10670  buttonp = new Fl_Button(795 - bw + 15, y, w, h, S("Cancel"));
10671  buttonp->callback(cancel_cb, (void*) this);
10672  // Resizable space between buttons
10673  bp = new Fl_Box(bw, gy, 795 - (2 * bw), gh);
10674  }
10675  gp->end();
10676  gp->resizable(bp);
10677  }
10678  articleGroup->end();
10679  articleGroup->resizable(articleDisplay);
10680  resizable(articleGroup);
10681 
10682  end();
10683  // --------------------------------------------------------------------
10684 
10685  // Set minimum window size
10686  size_range(2 * bw, 100, 0, 0);
10687 #if !CFG_DB_DISABLE
10688  Fl::visual(FL_DOUBLE | FL_INDEX);
10689 #endif // CFG_DB_DISABLE
10690  show();
10691  }
10692  }
10693 }
10694 
10695 
10696 // =============================================================================
10697 // Article source window destructor
10698 
10699 ArticleWindow::~ArticleWindow(void)
10700 {
10701  if(NULL != mimeData) { delete mimeData; }
10702  // Detach text buffers and destroy them
10703  articleDisplay->highlight_data(dummyTb, NULL, 0, 'A', NULL, NULL);
10704  if(articleStyle) delete articleStyle;
10705  if(styles) { delete[] styles; }
10706  articleDisplay->buffer(dummyTb);
10707  if(articleText) delete articleText;
10708  // Destroy alternative article hierarchy
10710 }
10711 
10712 
10713 // =============================================================================
10714 // Article source window save file
10715 
10716 void ArticleSrcWindow::save_cb_i(void)
10717 {
10718  int rv;
10719 
10720  SC("Do not use characters for the translation that cannot be")
10721  SC("converted to the ISO 8859-1 character set for this item.")
10722  SC("Leave the original string in place if in doubt.")
10723  const char* title = S("Save article source code");
10724  const char* suggested_pathname = core_suggest_pathname();
10725 
10726  fl_file_chooser_ok_label(S("Save"));
10727  if(NULL != suggested_pathname)
10728  {
10729  pathname = fl_file_chooser(title, "*", suggested_pathname, 0);
10730  if(NULL != pathname)
10731  {
10732  // Ask user for overwrite permission if file exists
10733  rv = gui_check_pathname(pathname);
10734  if (0 == rv)
10735  {
10736  // Save to file
10737  rv = core_save_to_file(pathname, srcArticle);
10738  if(rv)
10739  {
10740  SC("Do not use non-ASCII for the translation of this item")
10741  fl_message_title(S("Error"));
10742  fl_alert("%s", S("Operation failed"));
10743  }
10744  }
10745  }
10746  core_free((void*) suggested_pathname);
10747  }
10748 }
10749 
10750 
10751 // =============================================================================
10752 // Article source window constructor
10753 
10754 ArticleSrcWindow::ArticleSrcWindow(const char* article, const char* label) :
10755  UI_WINDOW_CLASS(795, 395, label)
10756 {
10757  Fl_Group* gp;
10758  Fl_Box* bp;
10759  Fl_Button* p;
10760  int y, w, h, gy, gh, bw;
10761  int w1, w2, h1, h2;
10762  std::size_t len;
10763  const char* ap = NULL;
10764  int i = 0;
10765  int buflen;
10766  unsigned int octet;
10767  char num[3];
10768  const char* style;
10769  int toggle = 0;
10770 
10771  copy_label(label);
10772 
10773  // Copy raw article content to local memory for "Save to file" operation
10774  len = std::strlen(article);
10775  srcArticle = new char[++len];
10776  std::strcpy(srcArticle, article);
10777 
10778  // Convert article content line breaks from canonical (RFC 822) form
10779  // to POSIX form
10780  srcText = new Fl_Text_Buffer();
10781  srcStyle = new Fl_Text_Buffer();
10782  ap = core_convert_canonical_to_posix(article, 0, 0);
10783  if(NULL == ap) { srcText->insert(0, "Conversion to POSIX form failed"); }
10784  else
10785  {
10786  srcText->insert(0, ap);
10787  // Replace control/non-ASCII octets with colored hexadecimal numbers
10788  buflen = srcText->length();
10789  while(buflen > i)
10790  {
10791  srcStyle->insert(i, "A");
10792  octet = (unsigned int) (unsigned char) srcText->byte_at(i);
10793  num[0] = (char) (unsigned char) octet;
10794  num[1] = 0;
10795  if( (0x0AU != octet && enc_ascii_check_printable(num))
10796  || 0x09U == octet )
10797  {
10798  enc_convert_octet_to_hex(num, octet);
10799  srcText->replace(i, i + 1, num);
10800  if(!toggle) { style = "BB"; } else { style = "CC"; }
10801  srcStyle->replace(i, i + 1, style);
10802  buflen = srcText->length();
10803  ++i;
10804  toggle = !toggle;
10805  }
10806  else { toggle = 0; }
10807  ++i;
10808  }
10809  core_free((void*) ap);
10810  gui_check_article(srcText);
10811  }
10812 
10813  // Configure article source code window
10814  callback(cancel_cb, (void*) this);
10815  set_modal();
10816 
10817  // Add widgets --------------------------------------------------------------
10818  begin();
10819 
10820  // Create widget group
10821  srcGroup = new Fl_Group(0, 0, 795, 395);
10822  srcGroup->begin();
10823  {
10824  // Calculate required widths and heights
10825  gui_set_default_font();
10826  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
10827  fl_measure(S("Save"), w2 = 0, h2 = 0);
10828  if(w1 > w2) { w = w1; } else { w = w2; }
10829  if(h1 > h2) { h = h1; } else { h = h2; }
10830  w += 30;
10831  h += 10;
10832  gh = h + 10;
10833  gy = 395 - gh;
10834  // Add text field
10835  srcDisplay = new Fl_Text_Display(0, 0, 795, 395 - gh);
10836  srcDisplay->textfont(FL_COURIER);
10837  srcDisplay->buffer(srcText);
10838  resizable(srcDisplay);
10839  // Create content and style buffers
10840  styles = new Fl_Text_Display::Style_Table_Entry[3];
10841  styles[0].color = FL_FOREGROUND_COLOR;
10842  styles[0].font = FL_COURIER;
10843  styles[0].size = srcDisplay->textsize();
10844  styles[0].attr = 0;
10845  styles[1].color = FL_COLOR_CUBE + (Fl_Color) 35;
10846  styles[1].font = FL_COURIER;
10847  styles[1].size = srcDisplay->textsize();
10848  styles[1].attr = 0;
10849  styles[2].color = FL_RED;
10850  styles[2].font = FL_COURIER;
10851  styles[2].size = srcDisplay->textsize();
10852  styles[2].attr = 0;
10853  srcDisplay->highlight_data(srcStyle, styles, 3, 'A', NULL, NULL);
10854  // Add button bar at bottom
10855  gp = new Fl_Group(0, gy, 795, gh);
10856  gp->begin();
10857  {
10858  bw = w + 30;
10859  y = gy + 5;
10860  new Fl_Box(0, gy, bw, gh);
10861  p = new Fl_Button(15, y, w, h, S("Save"));
10862  p->callback(save_cb, (void*) this);
10863  new Fl_Box(795 - bw, gy, bw, gh);
10864  p = new Fl_Button(795 - bw + 15, y, w, h, S("Cancel"));
10865  p->callback(cancel_cb, (void*) this);
10866  // Resizable space between buttons
10867  bp = new Fl_Box(bw, gy, 795 - (2 * bw), gh);
10868  }
10869  gp->end();
10870  gp->resizable(bp);
10871  }
10872  srcGroup->end();
10873  srcGroup->resizable(srcDisplay);
10874  resizable(srcGroup);
10875 
10876  end();
10877  // --------------------------------------------------------------------------
10878 
10879  // Set minimum window size
10880  size_range(2 * bw, 100, 0, 0);
10881 #if !CFG_DB_DISABLE
10882  Fl::visual(FL_DOUBLE | FL_INDEX);
10883 #endif // CFG_DB_DISABLE
10884  show();
10885 }
10886 
10887 
10888 // =============================================================================
10889 // Article source window destructor
10890 
10891 ArticleSrcWindow::~ArticleSrcWindow(void)
10892 {
10893  // Detach text buffers and destroy them
10894  srcDisplay->highlight_data(dummyTb, NULL, 0, 'A', NULL, NULL);
10895  delete srcStyle;
10896  delete[] styles;
10897  srcDisplay->buffer(dummyTb);
10898  delete srcText;
10899  delete[] srcArticle;
10900 }
10901 
10902 
10903 // =============================================================================
10904 // Compose window header parser
10905 //
10906 // \param[in] name Header field name
10907 // \param[out] start Start of header field (BOL)
10908 // \param[out] end End of header field (LF character at EOL)
10909 //
10910 // This method searches the header field \e name in the article header.
10911 
10912 int ComposeWindow::searchHeaderField(const char* name, int* start, int* end)
10913 {
10914  int res = -1;
10915  int rv;
10916  unsigned int c = (unsigned int) '\n';
10917  int sp = 0;
10918 
10919  while(1)
10920  {
10921  rv = compHeader->search_forward(sp, name, start, 1);
10922  if(1 == rv)
10923  {
10924  // Name match found => Verify that it starts at BOL
10925  if(0 < *start) { c = compHeader->char_at(*start - 1); }
10926  if((unsigned int) '\n' != c)
10927  {
10928  // No => Continue searching
10929  sp = *start + 1;
10930  continue;
10931  }
10932  else
10933  {
10934  // Yes => Search for EOL
10935  rv = compHeader->findchar_forward(*start, (unsigned int) '\n', end);
10936  if(1 == rv) { res = 0; }
10937  // Check for folding and include potential folded lines
10938  while(!res)
10939  {
10940  if((unsigned int) ' ' == compHeader->char_at(*end + 1)
10941  && (unsigned int) '\n' != compHeader->char_at(*end + 2))
10942  {
10943  rv = compHeader->findchar_forward(*end + 1,
10944  (unsigned int) '\n', end);
10945  if(1 != rv) { res = -1; }
10946  }
10947  else { break; }
10948  }
10949  }
10950  }
10951  break;
10952  }
10953 
10954  return(res);
10955 }
10956 
10957 // =============================================================================
10958 // Compose window header parser
10959 //
10960 // \param[in] name Header field name
10961 //
10962 // This method extracts the header field "name" from the article header.
10963 //
10964 // On success the caller is responsible for releasing the memory allocated for
10965 // the result using \c std::free() .
10966 //
10967 // \returns
10968 // - Pointer to result buffer
10969 // - NULL on error
10970 
10971 const char* ComposeWindow::extractHeaderField(const char* name)
10972 {
10973  char* res = NULL;
10974  int rv;
10975  int start;
10976  int end;
10977  int pos;
10978 
10979  rv = searchHeaderField(name, &start, &end);
10980  if(!rv)
10981  {
10982  // Search for start of header field body
10983  rv = compHeader->search_forward(start, ": ", &pos, 1);
10984  if(1 == rv)
10985  {
10986  pos += 2;
10987  res = compHeader->text_range(pos, end);
10988  }
10989  }
10990 
10991  return(res);
10992 }
10993 
10994 
10995 // =============================================================================
10996 // Compose window header parser
10997 //
10998 // \param[in] name Header field name
10999 // \param[in] new_body New body for header field \e name
11000 //
11001 // This method replaces the body of the header field \e name in the article
11002 // header.
11003 
11004 int ComposeWindow::replaceHeaderField(const char* name, const char* new_body)
11005 {
11006  int res = -1;
11007  int rv;
11008  int start;
11009  int end;
11010  int pos;
11011 
11012  rv = searchHeaderField(name, &start, &end);
11013  if(!rv)
11014  {
11015  // Search for start of header field body
11016  rv = compHeader->search_forward(start, ": ", &pos, 1);
11017  if(1 == rv)
11018  {
11019  pos += 2;
11020  if(end >= pos)
11021  {
11022  // Replace header field body
11023  compHeader->replace(pos, end, new_body);
11024  res = 0;
11025  }
11026  }
11027  }
11028 
11029  return(res);
11030 }
11031 
11032 
11033 // =============================================================================
11034 // Compose window header parser
11035 //
11036 // \param[in] name Header field name
11037 //
11038 // This method deletes the header field \e name in the article header.
11039 
11040 void ComposeWindow::deleteHeaderField(const char* name)
11041 {
11042  int rv;
11043  int start;
11044  int end;
11045 
11046  rv = searchHeaderField(name, &start, &end);
11047  if(!rv) { compHeader->remove(start, end + 1); }
11048 
11049  return;
11050 }
11051 
11052 
11053 // =============================================================================
11054 // Compose window body checker
11055 //
11056 // \param[in] body Article body to check
11057 //
11058 // This method verify that \e body contains own content.
11059 
11060 int ComposeWindow::checkArticleBody(const char* body)
11061 {
11062  int res = -1;
11063  const char* p;
11064  const char* sig_delim;
11065  const char* sig = NULL;
11066  std::size_t len;
11067  std::size_t i = 0;
11068  bool sol = false; // Start Of Line flag (ignore first line)
11069  bool check = false;
11070  std::size_t lines = 0;
11071 
11072  if(NULL != body)
11073  {
11074  // Calculate length without signature
11075  len = std::strlen(body);
11076  p = body;
11077  while(1)
11078  {
11079  // Attention: Overloaded and both prototypes different than in C!
11080  sig_delim = std::strstr(p, "\n-- \n");
11081  if(NULL == sig_delim) { break; }
11082  else { sig = p = &sig_delim[1]; }
11083  }
11084  if(NULL != sig) { len = (std::size_t) (sig - body); }
11085  // Search for own (not cited, non-whitespace) content
11086  for(i = 0; i < len; ++i)
11087  {
11088  if(0x0A == (int) body[i]) { sol = true; check = false; continue; }
11089  if(sol) { ++lines; }
11090  if(sol && '>' != body[i]) { check = true; }
11091  sol = false;
11092  if(check)
11093  {
11094  // Check content of non-citation line
11095  // SP and HT is not treated as relevant content
11096  if(' ' != body[i] && 0x09 != (int) body[i]) { res = 0; break; }
11097  }
11098  }
11099  // Special check for messages that contain only one line
11100  if(res && (std::size_t) 1 >= lines && '>' != body[0]) { res = 0; }
11101  }
11102 
11103  return(res);
11104 }
11105 
11106 
11107 // =============================================================================
11108 // Compose window Change subject button callback
11109 
11110 void ComposeWindow::change_cb_i(void)
11111 {
11112  const char* subject;
11113  const char* subject_new;
11114  char* p;
11115  const char* q;
11116  std::size_t len = 8; // Length of " (was: )"
11117 
11118  // Get old subject
11119  subject = subjectField->value();
11120  // Get new subject
11121  subject_new = fl_input("%s", "", S("New subject:"));
11122  if(NULL != subject_new)
11123  {
11124  if(std::strlen(subject_new))
11125  {
11126  // Check whether subject is changed again
11127  // Attention: Overloaded and both prototypes different than in C!
11128  q = std::strstr(subject, " (was: ");
11129  if(NULL != q)
11130  {
11131  // Yes => Preserve " (was: ..." part in parenthesis
11132  len += std::strlen(subject_new) + std::strlen(q);
11133  p = new char[++len];
11134  std::strcpy(p, subject_new);
11135  std::strcat(p, q);
11136  subjectField->value(p);
11137  delete[] p;
11138  }
11139  else
11140  {
11141  // No => Strip potential "Re: " prefix from old subject
11142  // Attention: Overloaded and both prototypes different than in C!
11143  q = std::strstr(subject, "Re: ");
11144  if(NULL != q) { subject = &subject[4]; }
11145  len += std::strlen(subject_new) + std::strlen(subject);
11146  p = new char[++len];
11147  std::strcpy(p, subject_new);
11148  std::strcat(p, " (was: ");
11149  std::strcat(p, subject);
11150  std::strcat(p, ")");
11151  subjectField->value(p);
11152  delete[] p;
11153  }
11154  }
11155  }
11156 }
11157 
11158 
11159 // =============================================================================
11160 // Compose window style update callback
11161 
11162 void ComposeWindow::style_update_cb_i(int pos, int nInserted, int nDeleted,
11163  Fl_Text_Buffer* style,
11164  Fl_Text_Editor* editor)
11165 {
11166  char sbuf[UI_STATIC_STYLE_BUFSIZE + (std::size_t) 1];
11167  char* buf;
11168  int start, end;
11169  int lines;
11170  int i;
11171  std::size_t len;
11172  int pil;
11173  int next;
11174  std::size_t p72, p78;
11175 
11176  // Check for parameter integrity
11177  if(0 > nInserted || 0 > nDeleted)
11178  {
11179  PRINT_ERROR("Invalid parameter in compose window style update CB");
11180  return;
11181  }
11182 
11183  // If this is just a selection change => Ignore
11184  if(!nInserted && !nDeleted) { return; }
11185 
11186  // Check whether text was inserted or deleted
11187  if(nInserted)
11188  {
11189  // Use buffer on stack if new data is small enough
11190  if(UI_STATIC_STYLE_BUFSIZE < (std::size_t) nInserted)
11191  {
11192  buf = new char[(std::size_t) (nInserted + 1)];
11193  }
11194  else { buf = sbuf; }
11195  std::memset((void*) buf, 'A', (std::size_t) nInserted);
11196  buf[nInserted] = 0;
11197  style->replace(pos, pos + nDeleted, buf);
11198  if(UI_STATIC_STYLE_BUFSIZE < (std::size_t) nInserted) { delete[] buf; }
11199  }
11200  else if(nDeleted)
11201  {
11202  style->remove(pos, pos + nDeleted);
11203  }
11204 
11205  // Restyle new data and up to next linefeed
11206  start = editor->buffer()->line_start(pos);
11207  end = editor->buffer()->line_end(pos + nInserted);
11208  lines = 1 + editor->buffer()->count_lines(pos, end);
11209  for(i = 0; i < lines; ++i)
11210  {
11211  buf = editor->buffer()->line_text(start);
11212  if(NULL == buf) { continue; }
11213  len = std::strlen(buf);
11214  if(len)
11215  {
11216  std::memset((void*) buf, 'A', len);
11217 
11218  next = start;
11219  pil = 0;
11220  p72 = 0;
11221  p78 = len;
11222  while((std::size_t) next - (std::size_t) start < len)
11223  {
11224  if(72 == pil) { p72 = (std::size_t) next - (std::size_t) start; }
11225  if(78 == pil++) { p78 = (std::size_t) next - (std::size_t) start; }
11226  next = editor->buffer()->next_char(next);
11227  }
11228  if(p72)
11229  {
11230  std::memset((void*) &buf[p72], 'B', p78 - p72);
11231  }
11232  if(len - p78)
11233  {
11234  std::memset((void*) &buf[p78], 'C', len - p78);
11235  }
11236 
11237  style->replace(start, start + (int) len, buf);
11238  }
11239  start = editor->buffer()->skip_lines(start, 1);
11240  std::free((void*) buf);
11241  }
11242 
11243  // Update editor content
11244  editor->redisplay_range(pos, end);
11245 }
11246 
11247 
11248 // =============================================================================
11249 // Compose window Cancel button callback
11250 
11251 void ComposeWindow::cancel_cb_i(Fl_Widget* w)
11252 {
11253  // Ignore escape key
11254  if(Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
11255  {
11256  return;
11257  }
11258  // Destroy compose window
11259  mainWindow->composeWindow = NULL;
11260  Fl::delete_widget(w);
11261  // Update main window state
11262  mainWindow->composeComplete();
11263 }
11264 
11265 
11266 // =============================================================================
11267 // Compose window Send button callback
11268 //
11269 // Note: Article is still in local (not canonical) form here
11270 
11271 void ComposeWindow::send_cb_i(void)
11272 {
11273  const char* fqdn = config[CONF_FQDN].val.s;
11274  char* p;
11275  std::size_t len;
11276  std::size_t i;
11277  std::size_t lenh;
11278  std::size_t lenb;
11279  int rv = 0;
11280  const char* q;
11281  const char* newsgroups;
11282  const char* subject;
11283  const char* organization;
11284  const char* msgid;
11285  const char* cancel_lock1;
11286  const char* cancel_lock;
11287  const char* date;
11288  const char* injection_date;
11289  const char* header;
11290  const char* body;
11291  const char* expires;
11292  bool free_subject = false;
11293  int start;
11294  int end;
11295  bool fup2_present = false;
11296 
11297  // Replace header field "Newsgroups" with potentially modified data
11298  newsgroups = newsgroupsField->value();
11299  len = std::strlen(newsgroups);
11300  if(!len)
11301  {
11302  SC("Do not use non-ASCII for the translation of this item")
11303  fl_message_title(S("Error"));
11304  fl_alert("%s", S("Newsgroups list is empty"));
11305  rv = -1;
11306  }
11307  else
11308  {
11309  // Check content according to RFC 5536
11310  q = newsgroups;
11311  for(i = 0; i < len; ++i)
11312  {
11313  if(',' == q[i] || '.' == q[i]
11314  || '+' == q[i] || '-' == q[i] || '_' == q[i])
11315  {
11316  continue;
11317  }
11318  if(0x30 <= q[i] && 0x39 >= q[i]) { continue; }
11319  if(0x41 <= q[i] && 0x5A >= q[i]) { continue; }
11320  if(0x61 <= q[i] && 0x7A >= q[i]) { continue; }
11321  rv = -1;
11322  break;
11323  }
11324  if(',' == q[0] || ',' == q[len - (std::size_t) 1]) { rv = -1; }
11325  if('.' == q[0] || '.' == q[len - (std::size_t) 1]) { rv = -1; }
11326  if(rv)
11327  {
11328  SC("Do not use non-ASCII for the translation of this item")
11329  fl_message_title(S("Error"));
11330  fl_alert("%s", S("Invalid content in Newsgroups header field"));
11331  }
11332  else
11333  {
11334  rv = replaceHeaderField("Newsgroups", newsgroups);
11335  if(rv)
11336  {
11337  SC("Do not use non-ASCII for the translation of this item")
11338  fl_message_title(S("Error"));
11339  fl_alert("%s", S("Replacement of Newsgroups in header failed"));
11340  rv = -1;
11341  }
11342  }
11343  }
11344 
11345  // Replace header field "Subject" with potentially modified data
11346  if(!rv)
11347  {
11348  subject = subjectField->value();
11349  if(!std::strlen(subject))
11350  {
11351  SC("Do not use non-ASCII for the translation of this item")
11352  fl_message_title(S("Error"));
11353  fl_alert("%s", S("Subject is empty"));
11354  rv = -1;
11355  }
11356  else
11357  {
11358  rv = enc_mime_word_encode(&q, subject, std::strlen("Subject: "));
11359  if(0 <= rv)
11360  {
11361  if(!rv) { free_subject = true; }
11362  subject = q;
11363  rv = replaceHeaderField("Subject", subject);
11364  if(rv)
11365  {
11366  SC("Do not use non-ASCII for the translation of this item")
11367  fl_message_title(S("Error"));
11368  fl_alert("%s", S("Replacement of Subject in header failed"));
11369  rv = -1;
11370  }
11371  if(free_subject) { enc_free((void*) subject); }
11372  }
11373  }
11374  }
11375 
11376  // Encode optional header field "Organization" that may be potentially present
11377  if(!rv)
11378  {
11379  organization = extractHeaderField("Organization");
11380  if(NULL != organization)
11381  {
11382  // Verify UTF-8 encoding
11383  rv = enc_uc_check_utf8(organization);
11384  if(!rv)
11385  {
11386  rv = enc_mime_word_encode(&q, organization,
11387  std::strlen("Organization: "));
11388  if(0 < rv) { rv = 0; }
11389  else if(!rv)
11390  {
11391  rv = replaceHeaderField("Organization", q);
11392  enc_free((void*) q);
11393  }
11394  }
11395  std::free((void*) organization);
11396  if(rv)
11397  {
11398  SC("Do not use non-ASCII for the translation of this item")
11399  fl_message_title(S("Error"));
11400  fl_alert("%s", S("Encoding of Organization header field failed"));
11401  rv = -1;
11402  }
11403  }
11404  }
11405 
11406  // Insert, replace or remove optional header fields before MIME declaration
11407  if(!rv)
11408  {
11409  rv = searchHeaderField("MIME-Version", &start, &end);
11410  if(rv)
11411  {
11412  SC("Do not use non-ASCII for the translation of this item")
11413  fl_message_title(S("Error"));
11414  fl_alert("%s", S("Bug: Mandatory MIME declaration not found"));
11415  rv = -1;
11416  }
11417  else
11418  {
11419  // Archive
11420  if(!archiveButton->value())
11421  {
11422  if(replaceHeaderField("Archive", "no"))
11423  {
11424  compHeader->insert(start, "Archive: no\n");
11425  }
11426  }
11427  else { deleteHeaderField("Archive"); }
11428  // Distribution
11429  q = distriField->value();
11430  if(NULL != q)
11431  {
11432  len = std::strlen(q);
11433  if(len)
11434  {
11435  // Check content (incomplete)
11436  for(i = 0; i < len; ++i)
11437  {
11438  if(',' == q[i] || '+' == q[i] || '-' == q[i] || '_' == q[i])
11439  {
11440  continue;
11441  }
11442  if(0x30 <= q[i] && 0x39 >= q[i]) { continue; }
11443  if(0x41 <= q[i] && 0x5A >= q[i]) { continue; }
11444  if(0x61 <= q[i] && 0x7A >= q[i]) { continue; }
11445  rv = -1;
11446  break;
11447  }
11448  if(',' == q[0] || ',' == q[len - (std::size_t) 1]) { rv = -1; }
11449  // RFC 5536 forbids use of "all" and recommends to not use
11450  // "world" (because this is the default behaviour, the header
11451  // field is not created in this case)
11452  p = (char*) std::malloc(len + (std::size_t) 1);
11453  if(NULL == p) { rv = -1; }
11454  else
11455  {
11456  // Convert content to lower case
11457  for(i = 0; i < len; ++i)
11458  {
11459  // Note: Behaviour is undefined if argument is not
11460  // representable as unsigned char or is not equal to EOF.
11461 #if defined(__cplusplus) && __cplusplus >= 199711L
11462  p[i] = (char) std::tolower((int) (unsigned char) q[i]);
11463 #else // defined(__cplusplus) && __cplusplus >= 199711L
11464  // Pre-C++98 compilers may have problems with the namespace
11465  p[i] = (char) tolower((int) (unsigned char) q[i]);
11466 #endif // defined(__cplusplus) && __cplusplus >= 199711L
11467  }
11468  p[len] = 0;
11469  // Check for "all"
11470  // Attention:
11471  // Overloaded and both prototypes different than in C!
11472  if(NULL != std::strstr(p, "all")) { rv = -1; }
11473  }
11474  // Check for error
11475  if(rv)
11476  {
11477  SC("Do not use non-ASCII for the translation of this item")
11478  fl_message_title(S("Error"));
11479  fl_alert("%s",
11480  S("Invalid content in Distribution header field"));
11481  }
11482  // Check for "world" (omit header field if found)
11483  // Attention: Overloaded and both prototypes different than in C!
11484  else if(NULL == std::strstr(p, "world"))
11485  {
11486  if(replaceHeaderField("Distribution", p))
11487  {
11488  compHeader->insert(start, "\n");
11489  compHeader->insert(start, p);
11490  compHeader->insert(start, "Distribution: ");
11491  }
11492  }
11493  else { deleteHeaderField("Distribution"); }
11494  std::free((void*) p);
11495  }
11496  }
11497  else { deleteHeaderField("Distribution"); }
11498  // Expires
11499  if(!rv)
11500  {
11501  expires = expireField->value();
11502  if(NULL != expires)
11503  {
11504  if(std::strlen(expires))
11505  {
11506  if(enc_convert_iso8601_to_timestamp(&q, expires))
11507  {
11508  SC("Do not use non-ASCII for the translation of this item")
11509  fl_message_title(S("Error"));
11510  fl_alert("%s", S("Invalid date for Expires header field"));
11511  rv = -1;
11512  }
11513  else
11514  {
11515  if(replaceHeaderField("Expires", q))
11516  {
11517  compHeader->insert(start, "\n");
11518  compHeader->insert(start, q);
11519  compHeader->insert(start, "Expires: ");
11520  }
11521  std::free((void*) q);
11522  }
11523  }
11524  }
11525  else { deleteHeaderField("Expires"); }
11526  }
11527  // Keywords
11528  if(!rv)
11529  {
11530  q = keywordField->value();
11531  if(NULL != q)
11532  {
11533  len = std::strlen(q);
11534  if(len)
11535  {
11536  // Check content
11537  for(i = 0; i < len; ++i)
11538  {
11539  // Accept a comma separated list of phrase subsets
11540  if(',' == q[i] || '!' == q[i] || '#' == q[i] || '$' == q[i]
11541  || '%' == q[i] || '&' == q[i] || '+' == q[i]
11542  || '-' == q[i] || '/' == q[i] || '=' == q[i]
11543  || '?' == q[i] || '^' == q[i] || '_' == q[i]
11544  || '{' == q[i] || '|' == q[i] || '}' == q[i]
11545  || '~' == q[i] || ' ' == q[i])
11546  {
11547  continue;
11548  }
11549  if(0x30 <= q[i] && 0x39 >= q[i]) { continue; } // 0-9
11550  if(0x41 <= q[i] && 0x5A >= q[i]) { continue; } // A-Z
11551  if(0x61 <= q[i] && 0x7A >= q[i]) { continue; } // a-z
11552  rv = -1;
11553  break;
11554  }
11555  // Reject comma or space as first and last character
11556  --len;
11557  if(',' == q[0] || ',' == q[len]
11558  || ' ' == q[0] || ' ' == q[len])
11559  {
11560  rv = -1;
11561  }
11562  if(rv)
11563  {
11564  SC("Do not use non-ASCII for the translation of this item")
11565  fl_message_title(S("Error"));
11566  fl_alert("%s",
11567  S("Invalid content in Keywords header field"));
11568  }
11569  else
11570  {
11571  if(replaceHeaderField("Keywords", q))
11572  {
11573  compHeader->insert(start, "\n");
11574  compHeader->insert(start, q);
11575  compHeader->insert(start, "Keywords: ");
11576  }
11577  }
11578  }
11579  }
11580  else { deleteHeaderField("Keywords"); }
11581  }
11582  // Followup-To
11583  if(!rv)
11584  {
11585  q = fup2Field->value();
11586  if(NULL != q)
11587  {
11588  len = std::strlen(q);
11589  if(len)
11590  {
11591  // Check content according to RFC 5536
11592  for(i = 0; i < len; ++i)
11593  {
11594  if(',' == q[i] || '.' == q[i]
11595  || '+' == q[i] || '-' == q[i] || '_' == q[i])
11596  {
11597  continue;
11598  }
11599  if(0x30 <= q[i] && 0x39 >= q[i]) { continue; }
11600  if(0x41 <= q[i] && 0x5A >= q[i]) { continue; }
11601  if(0x61 <= q[i] && 0x7A >= q[i]) { continue; }
11602  rv = -1;
11603  break;
11604  }
11605  if(',' == q[0] || ',' == q[len - (std::size_t) 1])
11606  {
11607  rv = -1;
11608  }
11609  if('.' == q[0] || '.' == q[len - (std::size_t) 1])
11610  {
11611  rv = -1;
11612  }
11613  if(rv)
11614  {
11615  SC("Do not use non-ASCII for the translation of this item")
11616  fl_message_title(S("Error"));
11617  fl_alert("%s",
11618  S("Invalid content in Followup-To header field"));
11619  }
11620  else
11621  {
11622  if(replaceHeaderField("Followup-To", q))
11623  {
11624  compHeader->insert(start, "\n");
11625  compHeader->insert(start, q);
11626  compHeader->insert(start, "Followup-To: ");
11627  fup2_present = true;
11628  }
11629  }
11630  }
11631  }
11632  else { deleteHeaderField("Followup-To"); }
11633  }
11634  }
11635  }
11636 
11637  // Generate new "Message-ID" header field for every posting attempt
11638  if(!rv)
11639  {
11640  msgid = extractHeaderField("Message-ID");
11641  if(NULL != msgid)
11642  {
11643  std::free((void*) msgid);
11644  if(std::strlen(fqdn))
11645  {
11646  msgid = core_get_msgid(fqdn);
11647  if(NULL != msgid)
11648  {
11649  replaceHeaderField("Message-ID", msgid);
11650  // Because the Cancel-Keys are based on the Message-ID,
11651  // a potential Cancel-Lock header field must be regenerated
11652  rv = searchHeaderField("Cancel-Lock", &start, &end);
11653  if(rv) { rv = 0; }
11654  else
11655  {
11656  deleteHeaderField("Cancel-Lock");
11657  // Create new Cancel-Lock header field
11658  cancel_lock1 = core_get_cancel_lock(CORE_CL_SHA1, msgid);
11659  cancel_lock = core_get_cancel_lock(CORE_CL_SHA256, msgid);
11660  if(NULL != cancel_lock1 || NULL != cancel_lock)
11661  {
11662  compHeader->insert(start, "\n");
11663  if(NULL != cancel_lock)
11664  {
11665  compHeader->insert(start, cancel_lock);
11666  compHeader->insert(start, " ");
11667  }
11668  if(NULL != cancel_lock1)
11669  {
11670  compHeader->insert(start, cancel_lock1);
11671  compHeader->insert(start, " ");
11672  }
11673  compHeader->insert(start, "Cancel-Lock:");
11674  core_free((void*) cancel_lock);
11675  core_free((void*) cancel_lock1);
11676  }
11677  }
11678  core_free((void*) msgid);
11679  }
11680  }
11681  }
11682  }
11683 
11684  // Update header field "Date" (if already present)
11685  if(!rv)
11686  {
11687  date = extractHeaderField("Date");
11688  if(NULL != date)
11689  {
11690  std::free((void*) date);
11691  date = core_get_datetime(0);
11692  rv = replaceHeaderField("Date", date);
11693  core_free((void*) date);
11694  if(rv)
11695  {
11696  SC("Do not use non-ASCII for the translation of this item")
11697  fl_message_title(S("Error"));
11698  fl_alert("%s", S("Updating Date header field failed"));
11699  rv = -1;
11700  }
11701  }
11702  }
11703 
11704  // Update header field "Injection-Date" (if already present)
11705  if(!rv)
11706  {
11707  injection_date = extractHeaderField("Injection-Date");
11708  if(NULL != injection_date)
11709  {
11710  std::free((void*) injection_date);
11711  injection_date = core_get_datetime(1);
11712  rv = replaceHeaderField("Injection-Date", injection_date);
11713  core_free((void*) injection_date);
11714  if(rv)
11715  {
11716  SC("Do not use non-ASCII for the translation of this item")
11717  fl_message_title(S("Error"));
11718  fl_alert("%s", S("Updating Injection-Date header field failed"));
11719  rv = -1;
11720  }
11721  }
11722  }
11723 
11724  // Check for Xpost (and display warning if no Fup2 is present)
11725  if(!rv)
11726  {
11727  // Attention: Overloaded and both prototypes different than in C!
11728  if(std::strchr(newsgroupsField->value(), (int) ',') && !fup2_present)
11729  {
11730  SC("Do not use non-ASCII for the translation of this item")
11731  fl_message_title(S("Warning"));
11732  rv = !fl_choice("%s", S("Cancel"),
11733  S("OK"), NULL,
11734  S("Xpost without Followup-To\nReally continue?"));
11735  }
11736  }
11737 
11738  // Combine header and body
11739  if(!rv)
11740  {
11741  header = compHeader->text();
11742  body = compText->text();
11743  if(NULL == header || NULL == body)
11744  {
11745  SC("Do not use non-ASCII for the translation of this item")
11746  fl_message_title(S("Error"));
11747  fl_alert("%s", S("Out of memory"));
11748  }
11749  else
11750  {
11751  lenh = std::strlen(header);
11752  lenb = std::strlen(body);
11753  // Check for empty body
11754  if(!lenb)
11755  {
11756  SC("Do not use non-ASCII for the translation of this item")
11757  fl_message_title(S("Error"));
11758  fl_alert("%s", S("Message body is empty"));
11759  }
11760  else
11761  {
11762  // Check body content to contain only citations
11763  if(checkArticleBody(body))
11764  {
11765  SC("Do not use non-ASCII for the translation of this item")
11766  fl_message_title(S("Error"));
11767  rv = !fl_choice("%s", S("Cancel"),
11768  S("OK"), NULL,
11769  S("Message body contains no own content"));
11770  }
11771  if(!rv)
11772  {
11773  // Post article
11774  p = (char*) std::malloc(lenh + lenb + (std::size_t) 1);
11775  if(NULL != p)
11776  {
11777  std::strcpy(p, header);
11778  std::strcat(p, body);
11779  // 'articlePost' will take responsibility for the memory
11780  mainWindow->articlePost(UI_CB_START, p);
11781  }
11782  }
11783  }
11784  std::free((void*) header);
11785  std::free((void*) body);
11786  // For code review: Do not 'free()' memory pointed to by 'p' here!
11787  }
11788  }
11789 }
11790 
11791 
11792 // =============================================================================
11793 // URI insertion callback
11794 
11795 void ComposeWindow::uri_insert_cb_i()
11796 {
11797  const char* scheme = uriSchemeField->value();
11798  enum enc_uri_scheme sch = ENC_URI_SCHEME_INVALID;
11799  const char* body_raw = uriBodyField->value();
11800  const char* body = NULL;
11801 
11802  // Select scheme
11803  if(std::strlen(scheme))
11804  {
11805  if(!std::strncmp(scheme, "http", 4))
11806  {
11807  sch = ENC_URI_SCHEME_HTTP;
11808  }
11809  else if(!std::strncmp(scheme, "ftp", 3))
11810  {
11811  sch = ENC_URI_SCHEME_FTP;
11812  }
11813  else if(!std::strncmp(scheme, "news", 4))
11814  {
11815  sch = ENC_URI_SCHEME_NEWS;
11816  }
11817  else if(!std::strncmp(scheme, "mailto", 6))
11818  {
11819  sch = ENC_URI_SCHEME_MAILTO;
11820  }
11821  }
11822  // Encode body
11823  body = enc_uri_percent_encode(body_raw, sch);
11824  if(NULL == body)
11825  {
11826  SC("Do not use non-ASCII for the translation of this item")
11827  fl_message_title(S("Error"));
11828  fl_alert("%s", S("Creation of URI failed"));
11829  }
11830  else
11831  {
11832  // Leading delimiter
11833  compEditor->insert("<");
11834  // Scheme
11835  compEditor->insert(scheme);
11836  // Colon delimiter
11837  compEditor->insert(":");
11838  // Authority designator
11839  if(ENC_URI_SCHEME_HTTP == sch || ENC_URI_SCHEME_FTP == sch)
11840  {
11841  compEditor->insert("//");
11842  }
11843  // Content (hier-part)
11844  compEditor->insert(body);
11845  uriBodyField->value("");
11846  // Trailing delimiter
11847  compEditor->insert(">");
11848  // Switch to content tab
11849  compTabs->value(compGroup);
11850  Fl::focus(compEditor);
11851  }
11852  if(body != body_raw) { enc_free((void*) body); }
11853 }
11854 
11855 
11856 // =============================================================================
11857 // Compose window constructor
11858 
11859 ComposeWindow::ComposeWindow(const char* label, const char* header,
11860  const char* article, const char* ca, struct core_article_header* hdr,
11861  bool super) : UI_WINDOW_CLASS(795, 395, label)
11862 {
11863  static const char label_ch[] = "1-----------------------------------------"
11864  "-------------------------72->|-78->|";
11865  Fl_Group* gp;
11866  Fl_Box* bp;
11867  Fl_Button* p;
11868  Fl_Box* chp;
11869  int th = 30; // Tab hight (including gap)
11870  int tg = 5; // Gap between tabs and cards
11871  int y, w, h, gy, gh, bw, sh, hh;
11872  int w1, w2, w3, w4, h1, h2, h3;
11873  const char* signature;
11874  const char* subject;
11875  const char* newsgroups; // Unfolded header field body
11876  const char** groups = NULL; // Array of individual group names
11877  int testGrp = 0;
11878  std::size_t i;
11879  unsigned int signature_warnings = 0;
11880  const char* uri_header;
11881  const char* distribution = NULL;
11882  int dist_msg_flag = 0;
11883  int rv;
11884  // Label strings
11885  SC("Preserve the spaces at beginning and end")
11886  const char* label_content = S(" Content ");
11887  const char* label_uri_encoder = S(" URI encoder ");
11888  const char* label_advanced = S(" Advanced ");
11889  SC("Preserve the colon")
11890  const char* label_subject = S("Subject:");
11891  const char* label_change = S("Change");
11892  const char* label_send = S("Send");
11893  const char* label_cancel = S("Cancel");
11894  const char* label_insert = S("Insert");
11895 
11896  // Configure article compose window
11897  copy_label(label);
11898  callback(cancel_cb, (void*) this);
11899 
11900  // Create text buffers
11901  compHeader = new Fl_Text_Buffer();
11902  compHeader->text(header);
11903  currentStyle = new Fl_Text_Buffer();
11904  compText = new Fl_Text_Buffer();
11905  compText->add_modify_callback(style_update_cb, (void*) this);
11906 
11907  // Add widgets --------------------------------------------------------------
11908  begin();
11909 
11910  // Create widget group
11911  compTabs = new Fl_Tabs(0, 0, 795, 395);
11912  compTabs->begin();
11913  {
11914  compGroup = new Fl_Group(0, th - tg, 795, 395 - th + tg, label_content);
11915  compGroup->begin();
11916  {
11917  // Calculate required widths and heights for buttons
11918  gui_set_default_font();
11919  fl_measure(label_send, w1 = 0, h1 = 0);
11920  fl_measure(label_cancel, w2 = 0, h2 = 0);
11921  if(w1 > w2) { w = w1; } else { w = w2; }
11922  if(h1 > h2) { h = h1; } else { h = h2; }
11923  w += 30;
11924  h += 10;
11925  gh = h + 10;
11926  gy = 395 - th - gh;
11927  // Add subject line
11928  sh = 30;
11929  subjectGroup = new Fl_Group(0, th, 795, sh);
11930  subjectGroup->begin();
11931  {
11932  gui_set_default_font();
11933  fl_measure(label_subject, w3 = 0, h3 = 0);
11934  w3 += 15;
11935  fl_measure(label_change, w4 = 0, h3 = 0);
11936  w4 += 30;
11937  subjectField = new Fl_Input(w3, th, 795 - w3 - w4, sh,
11938  label_subject);
11939  subjectField->align(FL_ALIGN_LEFT);
11940  subject = extractHeaderField("Subject");
11941  if(NULL != subject)
11942  {
11943  subjectField->value(subject);
11944  std::free((void*) subject);
11945  }
11946  p = new Fl_Button(795 - w4, th, w4, sh, label_change);
11947  p->tooltip(
11948  S("Change subject and cite old one with was: prefix in parenthesis")
11949  );
11950  p->callback(change_cb, (void*) this);
11951  }
11952  subjectGroup->end();
11953  subjectGroup->resizable(subjectField);
11954  // Add column hints
11955  hh = sh;
11956  chp = new Fl_Box(0, th + sh, 795, hh, label_ch);
11957  chp->labelfont(FL_COURIER);
11958  chp->align(FL_ALIGN_INSIDE | FL_ALIGN_LEFT);
11959  // Add text editor field
11960  compEditor = new Fl_Text_Editor(0, th + sh + hh, 795,
11961  395 - th - sh - hh - gh);
11962  compEditor->textfont(FL_COURIER);
11963  compEditor->show_insert_position();
11964  resizable(compEditor);
11965  // Create content and style buffers
11966  styles = new Fl_Text_Display::Style_Table_Entry[3];
11967  styles[0].color = FL_FOREGROUND_COLOR;
11968  styles[0].font = FL_COURIER;
11969  styles[0].size = compEditor->textsize();
11970  styles[0].attr = 0;
11971  styles[1].color = FL_COLOR_CUBE + (Fl_Color) 35;
11972  styles[1].font = FL_COURIER;
11973  styles[1].size = compEditor->textsize();
11974  styles[1].attr = 0;
11975  styles[2].color = FL_RED;
11976  styles[2].font = FL_COURIER;
11977  styles[2].size = compEditor->textsize();
11978  styles[2].attr = 0;
11979  compEditor->highlight_data(currentStyle, styles, 3, 'A', NULL, NULL);
11980  // Insert content
11981  compEditor->buffer(compText);
11982  if(NULL != article)
11983  {
11984  compText->insert(0, article);
11985  // Cite article if not superseding it
11986  if(!super)
11987  {
11988  if (NULL != ca) { gui_cite_content(compText, ca, hdr->groups); }
11989  }
11990  gui_check_article(compText);
11991  }
11992  if(super) { compEditor->insert_position(0); }
11993  else { compEditor->insert_position(compText->length()); }
11994  // Append signature (if available)
11995  signature = core_get_signature(&signature_warnings);
11996  if(NULL != signature)
11997  {
11998  if(!(signature_warnings & CORE_SIG_FLAG_INVALID))
11999  {
12000  if(!super) { compEditor->buffer()->append("\n"); }
12001  if(signature_warnings & CORE_SIG_FLAG_SEPARATOR)
12002  {
12003  compEditor->buffer()->append("-- \n");
12004  }
12005  compEditor->buffer()->append(signature);
12006  }
12007  core_free((void*) signature);
12008  }
12009  // Add button bar at bottom
12010  gp = new Fl_Group(0, th + gy, 795, gh);
12011  gp->begin();
12012  {
12013  bw = w + 30;
12014  y = th + gy + 5;
12015  new Fl_Box(0, th + gy, bw, gh);
12016  p = new Fl_Button(15, y, w, h, label_send);
12017  p->callback(send_cb, (void*) this);
12018  new Fl_Box(795 - bw, gy, bw, gh);
12019  p = new Fl_Button(795 - bw + 15, y, w, h, label_cancel);
12020  p->callback(cancel_cb, (void*) this);
12021  // Resizable space between buttons
12022  bp = new Fl_Box(bw, th + gy, 795 - (2 * bw), gh);
12023  }
12024  gp->end();
12025  gp->resizable(bp);
12026  }
12027  compGroup->end();
12028  compGroup->resizable(compEditor);
12029 
12030  // -----------------------------------------------------------------------
12031 
12032  uriEncGroup = new Fl_Group(0, th - tg, 795, 395 - th + tg,
12033  label_uri_encoder);
12034  uriEncGroup->begin();
12035  {
12036  y = th + tg + 10;
12037  uri_header = S("Percent encoder for clickable URIs");
12038  uriHeaderField = new Fl_Box(5, y, 795 - 10, sh, uri_header);
12039  uriHeaderField->labelfont(FL_HELVETICA_BOLD);
12040  y += 2 * sh;
12041  uriSchemeField = new Fl_Input_Choice(5, y, 795 - 10, sh,
12042  S("URI scheme:"));
12043  uriSchemeField->tooltip(S("Only the selectable schemes are supported"));
12044  uriSchemeField->align(FL_ALIGN_TOP_LEFT);
12045  uriSchemeField->add("http");
12046  uriSchemeField->add("https");
12047  uriSchemeField->add("ftp");
12048  uriSchemeField->add("news");
12049  uriSchemeField->add("mailto");
12050  uriSchemeField->input()->readonly(1);
12051  uriSchemeField->value(0); // Default to first entry in option list
12052  y += 2 * sh;
12053  uriBodyField = new Fl_Input(5, y, 795 - 10, sh, S("Body for URI:"));
12054  uriBodyField->align(FL_ALIGN_TOP_LEFT);
12055  // Add insert button at bottom
12056  y += 2 * sh;
12057  fl_measure(label_insert, w1 = 0, h1 = 0);
12058  w = w1 + 30;
12059  h = h1 + 10;
12060  gp = new Fl_Group(0, y, 795, h);
12061  gp->begin();
12062  {
12063  p = new Fl_Button(5, y, w, h, label_insert);
12064  p->callback(uri_insert_cb, (void*) this);
12065  fillSpace = new Fl_Box(5 + w, y, 795 - (10 + w), h);
12066  }
12067  gp->end();
12068  gp->resizable(fillSpace);
12069  // Fill unused space below button
12070  y += sh + 5;
12071  fillSpace = new Fl_Box(5, y, 795 - 10, 1);
12072  }
12073  uriEncGroup->end();
12074  uriEncGroup->resizable(fillSpace);
12075 
12076  // -----------------------------------------------------------------------
12077 
12078  advancedGroup = new Fl_Group(0, th - tg, 795, 395 - th + tg,
12079  label_advanced);
12080  advancedGroup->begin();
12081  {
12082  y = th + tg + 20;
12083  newsgroupsField = new Fl_Input(5, y, 795 - 10, sh, "Newsgroups:");
12084  newsgroupsField->align(FL_ALIGN_TOP_LEFT);
12085  newsgroupsField->tooltip(S("Comma separated list"));
12086  newsgroups = extractHeaderField("Newsgroups");
12087  if(NULL != newsgroups)
12088  {
12089  newsgroupsField->value(newsgroups);
12090  // Split body into group array and check for test groups
12091  groups = core_extract_groups(newsgroups);
12092  if(NULL != groups)
12093  {
12094  // Check group array (and destroy it again)
12095  i = 0;
12096  while(NULL != groups[i])
12097  {
12098  if(!testGrp) { testGrp = filter_check_testgroup(groups[i]); }
12099  core_free((void*) groups[i++]);
12100  }
12101  core_free((void*) groups);
12102  }
12103  // Release memory for header field body
12104  std::free((void*) newsgroups);
12105  }
12106  y += 2 * sh;
12107  fup2Field = new Fl_Input_Choice(5, y, 795 - 10, sh, "Followup-To:");
12108  fup2Field->align(FL_ALIGN_TOP_LEFT);
12109  fup2Field->tooltip(S("Comma separated list"));
12110  fup2Field->add("poster");
12111  if(NULL != hdr)
12112  {
12113  for(i = 0; i < UI_XPOST_LIMIT; ++i)
12114  {
12115  if(NULL == hdr->groups[i]) { break; }
12116  else
12117  {
12118  // Check for Xpost
12119  if(!i && NULL == hdr->groups[1]) { break; }
12120  fup2Field->add(hdr->groups[i]);
12121  }
12122  }
12123  if(super)
12124  {
12125  if(NULL != hdr->fup2 && std::strlen(hdr->fup2))
12126  {
12127  fup2Field->value(hdr->fup2);
12128  }
12129  }
12130  }
12131  y += 2 * sh;
12132  keywordField = new Fl_Input_Choice(5, y, 795 - 10, sh, "Keywords:");
12133  keywordField->align(FL_ALIGN_TOP_LEFT);
12134  keywordField->tooltip(S("Comma separated list"));
12135  if(!testGrp) { keywordField->add("ignore"); }
12136  else
12137  {
12138  // Set keywords for test group
12139  std::printf("%s: %s"
12140  "Setting keywords from config file for test group\n",
12141  CFG_NAME, MAIN_ERR_PREFIX);
12142  keywordField->value(config[CONF_TESTGRP_KWORDS].val.s);
12143  }
12144  y += 2 * sh;
12145  expireField = new Fl_Input(5, y, 795 - 10, sh, "Expires:");
12146  expireField->align(FL_ALIGN_TOP_LEFT);
12147  expireField->tooltip(
12148  S("Use date in ISO 8601 format YYYY-MM-DD")
12149  );
12150  y += 2 * sh;
12151  distriField = new Fl_Input_Choice(5, y, 795 - 10, sh, "Distribution:");
12152  distriField->align(FL_ALIGN_TOP_LEFT);
12153  if(NULL != hdr && NULL != hdr->dist && std::strlen(hdr->dist))
12154  {
12155  // Follow distribution for follow-up
12156  if(!super)
12157  {
12158  std::printf("%s: %sFollowing former distribution\n",
12159  CFG_NAME, MAIN_ERR_PREFIX);
12160  }
12161  distriField->value(hdr->dist);
12162  }
12163  else
12164  {
12165  // Follow suggestion of server
12166  rv = -1;
12167  newsgroups = extractHeaderField("Newsgroups");
12168  if(NULL != newsgroups)
12169  {
12170  // Split newsgroup field body into group array
12171  groups = core_extract_groups(newsgroups);
12172  // Match groups against distribution patterns
12173  rv = core_get_distribution(&distribution, groups);
12174  if(NULL != groups)
12175  {
12176  // Destroy group array again
12177  i = 0;
12178  while(NULL != groups[i]) { core_free((void*) groups[i++]); }
12179  core_free((void*) groups);
12180  }
12181  // Release memory for header field body
12182  std::free((void*) newsgroups);
12183  }
12184  if(!rv && config[CONF_DIST_SUGG].val.i)
12185  {
12186  dist_msg_flag = 1;
12187  std::printf("%s: %sSetting distribution as suggested\n",
12188  CFG_NAME, MAIN_ERR_PREFIX);
12189  distriField->value(distribution);
12190  core_free((void*) distribution);
12191  }
12192  else { distriField->value("world"); }
12193  }
12194  distriField->add("world");
12195  distriField->add("local");
12196  distriField->tooltip(
12197  S("Use country code or comma separated list of country codes")
12198  );
12199  y += sh + 5;
12200  archiveButton = new Fl_Check_Button(5, y, 795 - 10, sh, "Archive");
12201  archiveButton->set();
12202  // Fill unused space after resize
12203  y += sh + 5;
12204  fillSpace = new Fl_Box(5, y, 795 - 10, 1);
12205  }
12206  advancedGroup->end();
12207  advancedGroup->resizable(fillSpace);
12208  }
12209  compTabs->end();
12210  compTabs->resizable(compGroup);
12211 
12212  end();
12213  // --------------------------------------------------------------------------
12214 
12215  // Set focus
12216  if(!std::strlen(subjectField->value())) { Fl::focus(subjectField); }
12217  else { Fl::focus(compEditor); }
12218  // Set minimum window size
12219  size_range(4 * bw, 395, 0, 0);
12220 
12221  // Display potential message for distribution
12222  if(dist_msg_flag)
12223  {
12224  SC("Do not use non-ASCII for the translation of this item")
12225  fl_message_title(S("Note"));
12226  fl_message("%s", S("Distribution set as suggested by server"));
12227  }
12228 
12229  // Display potential warnings for signature
12230  if(signature_warnings & CORE_SIG_FLAG_INVALID)
12231  {
12232  SC("Do not use non-ASCII for the translation of this item")
12233  fl_message_title(S("Error"));
12234  fl_alert("%s", S("Signature has unsupported character set"));
12235  }
12236  else
12237  {
12238  if(signature_warnings & CORE_SIG_FLAG_LENGTH)
12239  {
12240  SC("Do not use non-ASCII for the translation of this item")
12241  fl_message_title(S("Note"));
12242  fl_message("%s", S("Signature should not be longer than 4 lines"));
12243  }
12244  }
12245 
12246  // Check for external editor
12247  if(!std::strlen(config[CONF_EDITOR].val.s))
12248  {
12249 #if !CFG_DB_DISABLE
12250  Fl::visual(FL_DOUBLE | FL_INDEX);
12251 #endif // CFG_DB_DISABLE
12252  show();
12253  }
12254  else
12255  {
12256  // Since FLTK 1.3.3 it is required to explicitly hide the new window
12257  hide();
12258  mainWindow->composeWindowLock = 1;
12259  UI_STATUS(S("Waiting for external editor ..."));
12260  }
12261 }
12262 
12263 
12264 // =============================================================================
12265 // Compose window destructor
12266 
12267 ComposeWindow::~ComposeWindow(void)
12268 {
12269  // Detach text buffers and destroy them
12270  compEditor->highlight_data(dummyTb, NULL, 0, 'A', NULL, NULL);
12271  delete currentStyle;
12272  delete[] styles;
12273  compEditor->buffer(dummyTb);
12274  delete compText;
12275  delete compHeader;
12276 }
12277 
12278 
12279 // =============================================================================
12280 //! \brief Init GUI
12281 //!
12282 //! \attention
12283 //! Remember that the locale must use either UTF-8 or ISO 8859-1 codeset or be
12284 //! the POSIX locale.
12285 //!
12286 //! \param[in] argc Command line argument count
12287 //! \param[in] argv Pointer to Command line argument array
12288 
12289 void ui_init(int argc, char** argv)
12290 {
12291  int rv;
12292  int w = 730;
12293  int h = 350;
12294  int x = 1;
12295  int y = 1;
12296  int tx = 230;
12297  int ty = 140;
12298  const char* text;
12299  Fl_Text_Buffer* info;
12300  const char* pass = NULL;
12301  const char* title_psfile;
12302 
12303  // Initialize MT support of FLTK
12304  rv = Fl::lock();
12305  if(rv)
12306  {
12307  PRINT_ERROR("No thread support available in FLTK");
12308  exitRequest = 1;
12309  }
12310  else
12311  {
12312  // This flag must be set after MT support of FLTK was initialized,
12313  // but before spawning any additional threads!
12314  lockingInitialized = true;
12315  // Start core (this will spawn a second thread)
12316  rv = core_init();
12317  if(rv) { exitRequest = 1; }
12318  }
12319  if(!exitRequest)
12320  {
12321  // Set global button labels
12322  SC("")
12323  SC("This section is for the button labels of popup windows")
12324  fl_cancel = S("Cancel");
12325  fl_close = S("Close");
12326  fl_ok = S("OK");
12327  fl_yes = S("Yes");
12328  fl_no = S("No");
12329  SC("")
12330 
12331  // Set global labels for text buffer
12332  SC("")
12333  SC("This section is for the text buffer warning messages")
12334  Fl_Text_Buffer::file_encoding_warning_message
12335  = S("Invalid encoding detected, trying to convert data");
12336 
12337  // Set global labels for file chooser
12338  SC("")
12339  SC("This section is for the file chooser labels")
12340  Fl_File_Chooser::all_files_label = S("*");
12341  Fl_File_Chooser::custom_filter_label = S("Custom filter");
12342  Fl_File_Chooser::existing_file_label
12343  = S("Please choose an existing file!");
12344  Fl_File_Chooser::favorites_label = S("Favorites");
12345  Fl_File_Chooser::add_favorites_label = S("Add to favorites");
12346  Fl_File_Chooser::manage_favorites_label = S("Manage favorites");
12347  Fl_File_Chooser::filesystems_label = S("File systems");
12348  Fl_File_Chooser::new_directory_label = S("New directory?");
12349  Fl_File_Chooser::new_directory_tooltip = S("Create a new directory.");
12350  Fl_File_Chooser::preview_label = S("Preview");
12351  Fl_File_Chooser::hidden_label = S("Show hidden files");
12352  Fl_File_Chooser::filename_label = S("Filename:");
12353  Fl_File_Chooser::save_label = S("Save");
12354  SC("The translation for this should not be much longer!")
12355  Fl_File_Chooser::show_label = S("Show:");
12356  SC("")
12357 
12358  // Set global labels for print dialog
12359  SC("")
12360  SC("This section is for the print dialog labels")
12361  SC("Do not use characters for the translation that cannot be")
12362  SC("converted to the ISO 8859-1 character set for this item.")
12363  SC("Leave the original string in place if in doubt.")
12364  Fl_Printer::dialog_title = S("Print");
12365  Fl_Printer::dialog_printer = S("Printer");
12366  Fl_Printer::dialog_range = S("Print range");
12367  Fl_Printer::dialog_copies = S("Copies");
12368  Fl_Printer::dialog_all = S("All");
12369  Fl_Printer::dialog_pages = S("Pages");
12370  Fl_Printer::dialog_from = S("From:");
12371  Fl_Printer::dialog_to = S("To:");
12372  Fl_Printer::dialog_properties = S("Properties ...");
12373  Fl_Printer::dialog_copyNo = S("Copies:");
12374  Fl_Printer::dialog_print_button = S("Print");
12375  Fl_Printer::dialog_cancel_button = S("Cancel");
12376  Fl_Printer::dialog_print_to_file = S("Print to file");
12377  SC("")
12378 
12379  // Set global labels for printer property dialog
12380  SC("")
12381  SC("This section is for the printer property dialog labels")
12382  SC("Do not use characters for the translation that cannot be")
12383  SC("converted to the ISO 8859-1 character set for this item.")
12384  SC("Leave the original string in place if in doubt.")
12385  Fl_Printer::property_title = S("Printer properties");
12386  Fl_Printer::property_pagesize = S("Page size:");
12387  Fl_Printer::property_mode = S("Output mode:");
12388  Fl_Printer::property_use = S("Use");
12389  Fl_Printer::property_save = S("Save");
12390  Fl_Printer::property_cancel = S("Cancel");
12391  SC("")
12392 
12393  // Set global label for print to file chooser dialog
12394  SC("")
12395  SC("This section is for the print to file chooser dialog label")
12396  SC("Do not use characters for the translation that cannot be")
12397  SC("converted to the ISO 8859-1 character set for this item.")
12398  SC("Leave the original string in place if in doubt.")
12399  title_psfile = S("Select a .ps file");
12400  SC("")
12401  // Following the FLTK documentation this should always work
12402  Fl_PostScript_File_Device::file_chooser_title = title_psfile;
12403 
12404  // Check clamp article count value
12405  if(UI_CAC_MIN > config[CONF_CAC].val.i)
12406  {
12407  config[CONF_CAC].val.i = UI_CAC_MIN;
12408  }
12409  if(UI_CAC_MAX < config[CONF_CAC].val.i)
12410  {
12411  config[CONF_CAC].val.i = UI_CAC_MAX;
12412  }
12413 
12414 #if CFG_CMPR_DISABLE
12415  // Disable compression negotiation if compiled without support
12417 #endif // CFG_CMPR_DISABLE
12418 
12419  // Construct main window
12420  dummyTb = new Fl_Text_Buffer(0, 0);
12421  mainWindow = new MainWindow(CFG_NAME);
12422 
12423  // Restore last window position and size
12424  mainWindow->size_range(w, h, 0, 0);
12425  if(config[CONF_POS_X].val.i >= x) { x = config[CONF_POS_X].val.i; }
12426  if(config[CONF_POS_Y].val.i >= y) { y = config[CONF_POS_Y].val.i; }
12427  if(config[CONF_SIZE_X].val.i >= w) { w = config[CONF_SIZE_X].val.i; }
12428  if(config[CONF_SIZE_Y].val.i >= h) { h = config[CONF_SIZE_Y].val.i; }
12429  mainWindow->resize(x, y, w, h);
12430  // Restore tiling
12431  tx = config[CONF_TILE_X].val.i;
12432  ty = config[CONF_TILE_Y].val.i;
12433  mainWindow->setTilingX(tx);
12434  mainWindow->setTilingY(ty);
12435  mainWindow->redraw();
12436 
12437  // Don't move message/choice windows under mouse cursor
12438  fl_message_hotspot(0);
12439 
12440 #if USE_WINDOW_ICON
12441  // Set default icon
12442  mainWindow->default_icon(&mainIcon);
12443 #endif // USE_WINDOW_ICON
12444 
12445  // Show GUI
12446  // The command line argument '-display' is parsed here. Ensure that the
12447  // connection to the X server is not opened before this point!
12448 #if !CFG_DB_DISABLE
12449  Fl::visual(FL_DOUBLE | FL_INDEX);
12450 #endif // CFG_DB_DISABLE
12451  mainWindow->show(argc, argv);
12452 
12453  // =======================================================================
12454 
12455  // Set default font
12456  gui_set_default_font();
12457 
12458  // Initialize current article
12459  info = new Fl_Text_Buffer;
12460  text = gui_greeting();
12461  info->text(text);
12462  delete[] text;
12463  mainWindow->articleUpdate(info);
12464 
12465  // Display warning if TLS module has detected potential vulnerability
12466  // Note: Do not move the NLS string inside the #if block
12467  text = S("Possible security vulnerability in TLS module detected!");
12468 #if CFG_USE_TLS
12469 # if !CFG_TLS_WARNING_DISABLE
12470  rv = tls_vulnerability_check(1);
12471 # else // !CFG_TLS_WARNING_DISABLE
12472  rv = tls_vulnerability_check(0);
12473 # endif // !CFG_TLS_WARNING_DISABLE
12474  if(0 > rv)
12475  {
12476  SC("Do not use non-ASCII for the translation of this item")
12477  fl_message_title(S("Warning"));
12478  fl_alert("%s", text);
12479  }
12480 #endif // CFG_USE_TLS
12481 
12482  // Check TLS certificate CRL update interval
12483  // Ask user whether automatic updates should be disabled if elapsed
12484 #if CFG_USE_TLS
12485  rv = tls_crl_update_check();
12486  if(rv)
12487  {
12488  SC("Do not use non-ASCII for the translation of this item")
12489  fl_message_title(S("Note"));
12490  rv = fl_choice("%s", S("Skip"),
12491  S("OK"), NULL,
12492  S("TLS certificate revocation list \x28\x43RL\x29\nupdate interval elapsed. Update now?"));
12493  if(!rv)
12494  {
12495  // Suppress automatic CRL updates for current session
12496  std::printf("%s: %sTLS certificate CRL updates suppressed\n",
12497  CFG_NAME, MAIN_ERR_PREFIX);
12499  }
12500  }
12501 #endif // CFG_USE_TLS
12502 
12503  // Ask for password if there is none but authentication is enabled
12504  if(!config[CONF_PASS].val.s[0]) { conf_ephemeral_passwd = 1; }
12505  if(1 == config[CONF_AUTH].val.i && conf_ephemeral_passwd)
12506  {
12507  do
12508  {
12509  pass = fl_password("%s", config[CONF_PASS].val.s, "Password:");
12510  if(NULL == pass) { exitRequest = 1; break; }
12511  else
12512  {
12514  if(!config[CONF_PASS].val.s[0])
12515  {
12516  SC("Do not use non-ASCII for the translation of this item")
12517  fl_message_title(S("Warning"));
12518  fl_alert("%s", S("Empty password is not supported"));
12519  }
12520  }
12521  } while(!config[CONF_PASS].val.s[0]);
12522  }
12523 
12524  // Initialize group tree via group list refresh timeout callback
12525  MainWindow::group_list_refresh_timer_cb((void*) mainWindow);
12526 
12527  //! \todo
12528  //! Workaround for "creeping window" problem without session manager seems
12529  //! not to be the "right" solution.
12530  //! If you know a better one, please report it.
12531  // Some window managers reposition the window while adding the borders.
12532  // First we assign the CPU so that this can happen.
12533  // Then we read back the new position and calculate correction offsets.
12534  // The stored correction offsets are used on shutdown to calculate the
12535  // positions that are saved in configfile.
12536  // The correction is limited to 20 points for the case that the window
12537  // manager moves the window to a completely different position.
12538  Fl::check();
12539  offset_correction_x = x - mainWindow->x();
12540  if(20 < offset_correction_x) { offset_correction_x = 0; }
12541  offset_correction_y = y - mainWindow->y();
12542  if(20 < offset_correction_y) { offset_correction_y = 0; }
12543  }
12544 }
12545 
12546 
12547 // =============================================================================
12548 //! \brief Drive GUI
12549 //!
12550 //! Assign the CPU to the GUI to process queued operations.
12551 //! Before this function is called, \ref ui_init() must be executed.
12552 //!
12553 //! \return
12554 //! - 0 on success
12555 //! - 1 to indicate an exit request from the GUI
12556 
12557 int ui_exec(void)
12558 {
12559  static const char* tmpfile = NULL;
12560  static long int editor_pid;
12561  static bool editor_term_lock = false;
12562  int res = 0;
12563  int rv;
12564  bool error;
12565 
12566  // Check whether main window is present
12567  if(NULL == mainWindow) { res = 1; }
12568  else
12569  {
12570  // Update protocol console
12571  if(NULL != protocolConsole) { protocolConsole->update(); }
12572 
12573  // Drive GUI
12574  Fl::wait(0.5);
12575 
12576  // State machine to handle child process for external editor
12577  if(NULL == mainWindow->composeWindow)
12578  {
12579  mainWindow->composeWindowLock = 0;
12580  }
12581  switch(mainWindow->composeWindowLock)
12582  {
12583  case 0:
12584  {
12585  // Check for exit request
12586  if(exitRequest) { res = 1; }
12587  break;
12588  }
12589  case 1: // Spawn child process for external editor
12590  {
12591  error = true;
12592  tmpfile = core_tmpfile_create();
12593  if(NULL != tmpfile)
12594  {
12595  rv = gui_save_to_file(tmpfile, mainWindow->composeWindow
12596  ->compEditor->buffer());
12597  if(!rv)
12598  {
12599  rv = ext_editor(tmpfile, 1, &editor_pid);
12600  if(!rv) { error = false; }
12601  }
12602  if(error)
12603  {
12604  core_tmpfile_delete(tmpfile);
12605  tmpfile = NULL;
12606  }
12607  }
12608  if(error)
12609  {
12610  UI_STATUS(S("Starting external editor failed."));
12611  mainWindow->composeWindowLock = 3;
12612  }
12613  else { mainWindow->composeWindowLock = 2; }
12614  break;
12615  }
12616  case 2: // Poll state of external editor
12617  {
12618  rv = ext_editor_status(editor_pid);
12619  if(-1 == rv)
12620  {
12621  // External editor still running
12622  if(exitRequest && !editor_term_lock)
12623  {
12624  // Terminate external editor
12625  ext_editor_terminate(editor_pid);
12626  editor_term_lock = true;
12627  }
12628  }
12629  else
12630  {
12631  // External editor child process has terminated
12632  if(rv) { UI_STATUS(S("External editor reported error.")); }
12633  else
12634  {
12635  // Import data from external editor
12636  UI_STATUS(S("External editor reported success."));
12637  rv = mainWindow->composeWindow
12638  ->compEditor->buffer()->loadfile(tmpfile);
12639  if(rv)
12640  {
12641  mainWindow->composeWindow
12642  ->compEditor->buffer()
12643  ->append(S("[Importing data from editor failed]"));
12644  }
12645  }
12646  core_tmpfile_delete(tmpfile);
12647  tmpfile = NULL;
12648  mainWindow->composeWindowLock = 3;
12649  }
12650  break;
12651  }
12652  case 3: // Show compose window
12653  {
12654  editor_term_lock = false;
12655  mainWindow->composeWindow->show();
12656  mainWindow->composeWindowLock = 0;
12657  break;
12658  }
12659  default:
12660  {
12661  PRINT_ERROR("Error in external editor state machine (bug)");
12662  res = 1;
12663  break;
12664  }
12665  }
12666  }
12667 
12668  return(res);
12669 }
12670 
12671 
12672 // =============================================================================
12673 //! \brief Shutdown GUI
12674 //!
12675 //! It is not allowed to call \ref ui_exec() after this function returns.
12676 
12677 void ui_exit(void)
12678 {
12679  if(NULL != mainWindow)
12680  {
12681  // Store current main window size
12682  config[CONF_POS_X].val.i = mainWindow->x() + offset_correction_x;
12683  config[CONF_POS_Y].val.i = mainWindow->y() + offset_correction_y;
12684  config[CONF_SIZE_X].val.i = mainWindow->w();
12685  config[CONF_SIZE_Y].val.i = mainWindow->h();
12686  config[CONF_TILE_X].val.i = mainWindow->getTilingX();
12687  config[CONF_TILE_Y].val.i = mainWindow->getTilingY();
12688 
12689  // Store group states
12690  if(main_debug)
12691  {
12692  printf("%s: %sExport group states for shutdown\n",
12693  CFG_NAME, MAIN_ERR_PREFIX);
12694  }
12695  mainWindow->groupStateExport();
12696  }
12697 
12698  // Shutdown core (and join with the core thread)
12699  core_exit();
12700  // MT lock of FLTK no longer required
12701  lockingInitialized = false;
12702 
12703  // The main window must exist until the core was shutdown (to be able to catch
12704  // all remaining callbacks)
12705  if(NULL != mainWindow)
12706  {
12707  // Destroy main window
12708  // This must be done after core shutdown (joining the core thread),
12709  // otherwise the core thread may create callbacks to the already destroyed
12710  // object.
12711  delete mainWindow;
12712 
12713  // This must be done last because the dummy buffer is required to destroy
12714  // the main window.
12715  delete dummyTb;
12716  }
12717 
12718 }
12719 
12720 
12721 // =============================================================================
12722 //! \brief Check whether locale use UTF-8 encoding
12723 //!
12724 //! Determine which codeset the locale uses that FLTK will set for \c LC_CTYPE
12725 //! after opening the X11 display.
12726 //!
12727 //! \attention
12728 //! This works even if the X11 display is not opened yet! This means the return
12729 //! value is valid before \ref ui_init() is called too.
12730 //!
12731 //! \return
12732 //! - 1 Locale uses UTF-8 encoding
12733 //! - 0 Locale uses other encoding
12734 
12736 {
12737  return(fl_utf8locale());
12738 }
12739 
12740 
12741 // =============================================================================
12742 //! \brief Wakeup callback (called by core thread after operation has finished)
12743 //!
12744 //! \param[in] cookie Cookie that was assigned to the operation by GUI
12745 
12746 void ui_wakeup(unsigned int cookie)
12747 {
12748  int rv;
12749 
12750  // Schedule the callback indicated by 'cookie' and awake UI thread
12751  switch(cookie)
12752  {
12753  case UI_CB_COOKIE_SERVER:
12754  {
12755  rv = Fl::awake(MainWindow::serverconf_cb, (void*) mainWindow);
12756  break;
12757  }
12758  case UI_CB_COOKIE_GROUPLIST:
12759  {
12760  rv = Fl::awake(MainWindow::subscribe_cb1, (void*) mainWindow);
12761  break;
12762  }
12763  case UI_CB_COOKIE_GROUPLABELS:
12764  {
12765  rv = Fl::awake(MainWindow::subscribe_cb2, (void*) mainWindow);
12766  break;
12767  }
12768  case UI_CB_COOKIE_GROUPPROPOSAL:
12769  {
12770  rv = Fl::awake(MainWindow::groupproposal_cb, (void*) mainWindow);
12771  break;
12772  }
12773  case UI_CB_COOKIE_GROUPINFO1:
12774  {
12775  rv = Fl::awake(MainWindow::refresh_cb1, (void*) mainWindow);
12776  break;
12777  }
12778  case UI_CB_COOKIE_GROUPINFO2:
12779  {
12780  rv = Fl::awake(MainWindow::refresh_cb2, (void*) mainWindow);
12781  break;
12782  }
12783  case UI_CB_COOKIE_GROUP:
12784  {
12785  rv = Fl::awake(MainWindow::group_cb, (void*) mainWindow);
12786  break;
12787  }
12788  case UI_CB_COOKIE_OVERVIEW:
12789  {
12790  rv = Fl::awake(MainWindow::overview_cb, (void*) mainWindow);
12791  break;
12792  }
12793  case UI_CB_COOKIE_HEADER:
12794  {
12795  rv = Fl::awake(MainWindow::header_cb, (void*) mainWindow);
12796  break;
12797  }
12798  case UI_CB_COOKIE_BODY:
12799  {
12800  rv = Fl::awake(MainWindow::body_cb, (void*) mainWindow);
12801  break;
12802  }
12803  case UI_CB_COOKIE_MOTD:
12804  {
12805  rv = Fl::awake(MainWindow::motd_cb, (void*) mainWindow);
12806  break;
12807  }
12808  case UI_CB_COOKIE_ARTICLE:
12809  {
12810  rv = Fl::awake(MainWindow::article_cb, (void*) mainWindow);
12811  break;
12812  }
12813  case UI_CB_COOKIE_SRC:
12814  {
12815  rv = Fl::awake(MainWindow::src_cb, (void*) mainWindow);
12816  break;
12817  }
12818  case UI_CB_COOKIE_POST:
12819  {
12820  rv = Fl::awake(MainWindow::post_cb, (void*) mainWindow);
12821  break;
12822  }
12823  default:
12824  {
12825  PRINT_ERROR("Can't assign cookie to callback function");
12826  rv = -1;
12827  break;
12828  }
12829  }
12830  if(rv)
12831  {
12832  PRINT_ERROR("Registering awake callback failed (fatal error)");
12833  exitRequest = 1;
12834  }
12835 }
12836 
12837 
12838 // =============================================================================
12839 //! \brief Lock for multithread support
12840 //!
12841 //! This function must be called by other threads before they call an OS
12842 //! function that is not guaranteed to be thread-safe. Prominent examples are:
12843 //! - \c getenv()
12844 //! - \c gethostbyname()
12845 //! - \c localtime()
12846 //! - \c readdir()
12847 //!
12848 //! FLTK documentation for MT locking:
12849 //! <br>
12850 //! https://www.fltk.org/doc-1.3/advanced.html#advanced_multithreading
12851 //!
12852 //! \return
12853 //! - 0 on success
12854 //! - Negative value on error
12855 
12856 int ui_lock(void)
12857 {
12858  int res = 0;
12859 
12860  // Only the UI thread exist until locking is initialized
12861  if(lockingInitialized)
12862  {
12863  res = Fl::lock();
12864  if(res) { res = -1; }
12865  }
12866 
12867  return(res);
12868 }
12869 
12870 
12871 // =============================================================================
12872 //! \brief Unlock for multithread support
12873 //!
12874 //! \return
12875 //! - 0 on success
12876 //! - Negative value on error
12877 
12878 int ui_unlock(void)
12879 {
12880  // Only the UI thread exist until locking is initialized
12881  if(lockingInitialized)
12882  {
12883  Fl::unlock();
12884  }
12885 
12886  return(0);
12887 }
12888 
12889 
12890 //! @}
12891 
12892 // EOF
core_mutex_lock
void core_mutex_lock(void)
Lock mutex for data object (exported for UI)
Definition: core.c:6574
CONF_COLOR_SIGNATURE
Definition: conf.h:83
core_hierarchy_element::header
struct core_article_header * header
Definition: core.h:143
core_suggest_pathname
const char * core_suggest_pathname(void)
Suggest pathname to save something to a file (exported for UI)
Definition: core.c:5418
core_export_group_states
int core_export_group_states(size_t num, struct core_groupstate *list)
Store states of subscribed groups (exported for UI)
Definition: core.c:3563
core_groupdesc
#define core_groupdesc
Group descriptor structure.
Definition: core.h:30
tls_vulnerability_check
int tls_vulnerability_check(int)
Check TLS subsystem for known vulnerabilities.
Definition: tls.c:1863
core_create_hierarchy_from_overview
void core_create_hierarchy_from_overview(struct core_groupstate *group, struct core_range *range, const char *overview)
Create article hierarchy from header overview (exported for UI)
Definition: core.c:5123
enc_mime_ct::subtype
enum enc_mime_ct_subtype subtype
Definition: encoding.h:115
log_free
void log_free(void *)
Free an object allocated by logging module.
Definition: log.c:202
CONF_AUTH
Definition: conf.h:41
core_convert_pathname_to_locale
const char * core_convert_pathname_to_locale(const char *pathname)
Convert pathname to codeset of locale (exported for UI)
Definition: core.c:6261
filter_get_score
int filter_get_score(const struct core_hierarchy_element *he)
Get article score.
Definition: filter.c:1685
core_convert_canonical_to_posix
const char * core_convert_canonical_to_posix(const char *s, int rcr, int rlf)
Convert from canonical (RFC 822) to local (POSIX) form.
Definition: core.c:4973
filter_match_own
int filter_match_own(const struct core_hierarchy_element *he)
Check for own article.
Definition: filter.c:1595
CONF_ORGANIZATION
Definition: conf.h:52
ENC_URI_SCHEME_HTTP
Definition: encoding.h:132
CONF_DIST_SUGG
Definition: conf.h:71
CONF_ONLYUR
Definition: conf.h:54
ENC_CT_VIDEO
Definition: encoding.h:29
ENC_CS_ASCII
Definition: encoding.h:62
ENC_CTE_BIN
Definition: encoding.h:53
CORE_HE_FLAG_OVER
#define CORE_HE_FLAG_OVER
Definition: core.h:194
enc_mime_mpe
Locations of MIME multipart entities.
Definition: encoding.h:122
core_get_group_list
int core_get_group_list(unsigned int cookie)
Get list of available newsgroups (exported for UI)
Definition: core.c:3325
core_data::result
int result
Definition: core.h:68
enc_free
void enc_free(void *p)
Free an object allocated by encoding module.
Definition: encoding.c:7856
core_grouplabel
#define core_grouplabel
Group label structure.
Definition: core.h:36
ui_wakeup
void ui_wakeup(unsigned int cookie)
Wakeup callback (called by core thread after operation has finished)
Definition: gui.cxx:12746
CONF_SERVICE
Definition: conf.h:39
core_data::size
size_t size
Definition: core.h:69
core_range::next
struct core_range * next
Definition: core.h:78
CONF_COLOR_LEVEL2
Definition: conf.h:87
enc_convert_to_utf8_nfc
const char * enc_convert_to_utf8_nfc(enum enc_mime_cs charset, const char *s)
Convert string from supported character set to Unicode (UTF-8 NFC)
Definition: encoding.c:4777
tls_crl_update_check
int tls_crl_update_check(void)
Check whether CRL update interval has elapsed.
Definition: tls.c:2842
core_anum_t
#define core_anum_t
Article number data type (value zero is always reserved)
Definition: core.h:24
CORE_HIERARCHY_GETROOT
Definition: core.h:159
log_get_logpathname
const char * log_get_logpathname(void)
Get logfile pathname.
Definition: log.c:55
CONF_EDITOR
Definition: conf.h:56
enc_uri_percent_encode
const char * enc_uri_percent_encode(const char *s, enum enc_uri_scheme sch)
Percent encoding for URI content.
Definition: encoding.c:7390
UI_BUSY
#define UI_BUSY()
Display "Busy" in main window progress bar.
Definition: gui.cxx:1676
ENC_URI_SCHEME_NEWS
Definition: encoding.h:134
core_convert_posix_to_canonical
const char * core_convert_posix_to_canonical(const char *s)
Convert from local (POSIX) to canonical (RFC 822) form.
Definition: core.c:4993
ENC_CT_MULTIPART
Definition: encoding.h:30
enc_mime_flowed_decode
const char * enc_mime_flowed_decode(const char *s, unsigned int delsp, unsigned int insline)
Decode MIME "text/plain" content with "format=flowed" parameter.
Definition: encoding.c:6844
core_time_t
unsigned long int core_time_t
Time in seconds since the epoche (in terms of POSIX.1)
Definition: core.h:54
core_get_subscription_proposals
int core_get_subscription_proposals(unsigned int cookie)
Get subscription proposals (exported for UI)
Definition: core.c:4110
enc_mime_save_to_file
int enc_mime_save_to_file(const char *pn, enum enc_mime_cte cte, const char *entity)
Decode MIME content transfer encoding and save to file.
Definition: encoding.c:6704
enc_rot13
void enc_rot13(char *data)
Encode or decode data with ROT13 algorithm.
Definition: encoding.c:3709
CORE_HIERARCHY_ADD
Definition: core.h:160
core_hierarchy_element::children
size_t children
Definition: core.h:147
core_get_homedir
const char * core_get_homedir(void)
Get home directory of user (exported for UI)
Definition: core.c:5384
core_hierarchy_element::child
struct core_hierarchy_element ** child
Definition: core.h:149
CONF_COLOR_HYPERLINK
Definition: conf.h:85
core_range
Article range linked list element.
Definition: core.h:74
enc_mime_get_cte
enum enc_mime_cte enc_mime_get_cte(const char *hf_body)
Decode content transfer encoding description.
Definition: encoding.c:6522
core_reset_group_states
int core_reset_group_states(unsigned int cookie)
Reset states of subscribed groups (exported for UI)
Definition: core.c:3520
core_groupstate
Group description.
Definition: core.h:82
CONF_POS_Y
Definition: conf.h:33
core_article_header::reply2
const char * reply2
Definition: core.h:116
config
struct conf config[CONF_NUM]
Global configuration.
Definition: conf.c:63
ENC_CTE_8BIT
Definition: encoding.h:52
enc_convert_ascii_to_anum
int enc_convert_ascii_to_anum(core_anum_t *result, const char *wm, int len)
Convert number from ASCII to numerical format.
Definition: encoding.c:3621
core_get_distribution
int core_get_distribution(const char **dist, const char **groups)
Get distribution suggestions (exported for UI)
Definition: core.c:3893
core_hierarchy_manager
int core_hierarchy_manager(struct core_hierarchy_element **hier, enum core_hierarchy_action action, core_anum_t anum,...)
Manage article hierarchy in memory (exported for UI)
Definition: core.c:5056
core_article_header::mime_ct
const char * mime_ct
Definition: core.h:122
core_unsubscribe_group
int core_unsubscribe_group(size_t *num, struct core_groupstate **list, size_t *index)
Remove group from list (exported for UI)
Definition: core.c:3476
core_free
void core_free(void *p)
Free an object allocated by core (exported for UI)
Definition: core.c:6637
CORE_SIG_FLAG_INVALID
#define CORE_SIG_FLAG_INVALID
Definition: core.h:200
core_article_header::dist
const char * dist
Definition: core.h:120
core_get_cancel_lock
const char * core_get_cancel_lock(unsigned int scheme, const char *mid)
Create Cancel-Lock for Message-ID (exported for UI)
Definition: core.c:5934
conf_entry_val::s
char * s
Definition: conf.h:105
UI_HDR_BUFSIZE
#define UI_HDR_BUFSIZE
Size of buffer for header field creation.
Definition: gui.cxx:199
enc_percent_decode
int enc_percent_decode(char *s, int clean)
Percent decoder.
Definition: encoding.c:7306
core_mark_as_read
void core_mark_as_read(struct core_groupstate *group, core_anum_t article)
Mark article as read (exported for UI)
Definition: core.c:4773
core_post_article
int core_post_article(const char *article, unsigned int cookie)
Post article (exported for UI)
Definition: core.c:4502
core_article_header::subject
const char * subject
Definition: core.h:112
core_init
int core_init(void)
Initialize core (exported for UI)
Definition: core.c:6653
CONF_FROM
Definition: conf.h:46
core_article_header::uagent
const char * uagent
Definition: core.h:117
CONF_SIZE_X
Definition: conf.h:34
conf_string_replace
int conf_string_replace(struct conf *cfg, const char *s)
Replace configuration string.
Definition: conf.c:918
ENC_BO_BUFLEN
#define ENC_BO_BUFLEN
Buffer size for multipart boundary strings.
Definition: encoding.h:176
ext_handler_uri
int ext_handler_uri(const char *uri, int *invalid)
Start external handler for URI.
Definition: extutils.c:391
core_exit
void core_exit(void)
Shutdown core (exported for UI)
Definition: core.c:6711
enc_mime_ct::type
enum enc_mime_ct_type type
Definition: encoding.h:114
ENC_CT_APPLICATION
Definition: encoding.h:32
core_hierarchy_element::anum
core_anum_t anum
Definition: core.h:139
core_range::first
core_anum_t first
Definition: core.h:76
CORE_CL_SHA256
#define CORE_CL_SHA256
Definition: core.h:209
CONF_REFRESH_INTERVAL
Definition: conf.h:81
CONF_COLOR_LEVEL1
Definition: conf.h:86
ui_unlock
int ui_unlock(void)
Unlock for multithread support.
Definition: gui.cxx:12878
enc_convert_lines_to_string
void enc_convert_lines_to_string(char *l, unsigned long int l_raw)
Convert number of lines to string.
Definition: encoding.c:3136
core_get_subscribed_group_info
int core_get_subscribed_group_info(const size_t *num, struct core_groupstate **list, unsigned int cookie)
Get information about subscribed groups (exported for UI)
Definition: core.c:3693
core_article_header::groups
const char ** groups
Definition: core.h:110
enc_mime_mpe::len
size_t len
Definition: encoding.h:125
core_tmpfile_delete
void core_tmpfile_delete(const char *pathname)
Delete temporary file (exported for UI)
Definition: core.c:6542
core_sort_group_list
int core_sort_group_list(void)
Alphabetically sort group list (exported for UI)
Definition: core.c:3424
UI_COLOR_RADIO_BUTTON
#define UI_COLOR_RADIO_BUTTON
Color for selected radio button.
Definition: gui.cxx:145
main_debug
int main_debug
Enable additional debug output if nonzero.
Definition: main.cxx:64
core_get_motd
int core_get_motd(unsigned int cookie)
Get message of the day (exported for UI)
Definition: core.c:3830
core_subscribe_group
int core_subscribe_group(const char *name)
Store group subscription (exported for UI)
Definition: core.c:3444
enc_convert_posix_to_iso8601
int enc_convert_posix_to_iso8601(char *isodate, core_time_t pts)
Convert POSIX timestamp to ISO 8601 conformant local date and time.
Definition: encoding.c:3359
core_mutex_unlock
void core_mutex_unlock(void)
Unlock mutex for data object (exported for UI)
Definition: core.c:6590
enc_extract_addr_spec
const char * enc_extract_addr_spec(const char *mailbox)
Extract addr-spec token from RFC 5322 mailbox.
Definition: encoding.c:3808
CONF_COLOR_EXTERNAL
Definition: conf.h:84
CONF_REPLYTO
Definition: conf.h:47
CONF_QUOTESTYLE
Definition: conf.h:59
UI_XPOST_LIMIT
#define UI_XPOST_LIMIT
Maximum number of groups for crossposting.
Definition: gui.cxx:205
CONF_COLOR_LEVEL3
Definition: conf.h:88
core_get_group_labels
int core_get_group_labels(unsigned int cookie)
Get list of newsgroup labels (exported for UI)
Definition: core.c:3384
CONF_UTVIEW_AN
Definition: conf.h:68
enc_ascii_check_printable
int enc_ascii_check_printable(const char *s)
Check for printable ASCII characters.
Definition: encoding.c:4000
UI_PROGRESS
#define UI_PROGRESS(s, e)
Update value of main window progress bar.
Definition: gui.cxx:1689
CONF_UNREAD_IN_NEXT_GROUP
Definition: conf.h:82
core_article_header::from
const char * from
Definition: core.h:111
core_article_header::mime_cte
const char * mime_cte
Definition: core.h:123
CORE_HIERARCHY_INIT
Definition: core.h:158
core_get_msgid
const char * core_get_msgid(const char *fqdn)
Get globally unique Message-ID (exported for UI)
Definition: core.c:5638
core_destroy_subscribed_group_info
void core_destroy_subscribed_group_info(struct core_groupdesc **list)
Destructor for subscribed group information (exported for UI)
Definition: core.c:3740
conf_entry_val::i
int i
Definition: conf.h:104
core_article_header::x_mailer
const char * x_mailer
Definition: core.h:126
core_destroy_subscribed_group_states
void core_destroy_subscribed_group_states(size_t *num, struct core_groupstate **list)
Destructor for subscribed group states (exported for UI)
Definition: core.c:3649
CONF_SCORE_KILL_TH
Definition: conf.h:90
core_article_header::refs
const char ** refs
Definition: core.h:119
CONF_CAC
Definition: conf.h:45
UI_READY
#define UI_READY()
Clear main window progress bar.
Definition: gui.cxx:1664
ui_exec
int ui_exec(void)
Drive GUI.
Definition: gui.cxx:12557
core_article_header::org
const char * org
Definition: core.h:118
ENC_CT_AUDIO
Definition: encoding.h:28
UI_COLOR_PROGRESS_BAR
#define UI_COLOR_PROGRESS_BAR
Color for progress bar.
Definition: gui.cxx:143
ext_editor_status
int ext_editor_status(long int editor_pid)
Poll status of external editor.
Definition: extutils.c:607
enc_mime_message
size_t enc_mime_message(const char *s, size_t len, struct enc_mime_mpe **mpe)
Extract MIME encapsulated message.
Definition: encoding.c:7156
ext_pp_filter
const char * ext_pp_filter(const char *article)
External post processing filter for outgoing articles.
Definition: extutils.c:686
CONF_COMPRESSION
Definition: conf.h:72
filter_check_testgroup
int filter_check_testgroup(const char *group)
Check for test group.
Definition: filter.c:1535
core_check_file_exist
int core_check_file_exist(const char *pathname)
Check wheter file exists (exported for UI)
Definition: core.c:6352
UI_STYLES_LEN
#define UI_STYLES_LEN
Number of styles for article content syntax highlighting.
Definition: gui.cxx:149
MAIN_ERR_PREFIX
#define MAIN_ERR_PREFIX
Message prefix for MAIN module.
Definition: gui.cxx:134
CORE_CL_SHA1
#define CORE_CL_SHA1
Definition: core.h:208
ENC_DELIMITER
#define ENC_DELIMITER
Delimiter string to print between article header and body parts.
Definition: encoding.h:179
core_article_header
Article header fields supported by core.
Definition: core.h:107
ui_get_locale_utf8
int ui_get_locale_utf8(void)
Check whether locale use UTF-8 encoding.
Definition: gui.cxx:12735
core_groupstate::name
const char * name
Definition: core.h:84
enc_uc_check_utf8
int enc_uc_check_utf8(const char *s)
Verify UTF-8 encoding.
Definition: encoding.c:4140
CONF_INITIAL_GREETING
Definition: conf.h:80
enc_mime_decode
const char * enc_mime_decode(enum enc_mime_cte cte, enum enc_mime_cs charset, const char *s)
Decode MIME text content to UTF-8 NFC.
Definition: encoding.c:6789
nls_loc
char nls_loc[6]
Current NLS locale.
Definition: nls.c:76
enc_convert_anum_to_ascii
int enc_convert_anum_to_ascii(char result[17], size_t *len, core_anum_t wm)
Convert article number from numerical format to ASCII.
Definition: encoding.c:3575
core_hierarchy_element
Node in article hierarchy.
Definition: core.h:136
core_mark_as_unread
void core_mark_as_unread(struct core_groupstate *group, core_anum_t article)
Mark article as unread (exported for UI)
Definition: core.c:4878
conf_integer_check
int conf_integer_check(enum conf_entry id, int min, int max)
Check integer value against lower and upper bounds.
Definition: conf.c:964
enc_mime_ct::charset
enum enc_mime_cs charset
Definition: encoding.h:116
PRINT_ERROR
#define PRINT_ERROR(s)
Prepend module prefix and print error message.
Definition: main.h:19
core_article_header::supers
const char * supers
Definition: core.h:114
CONF_TS_LTIME
Definition: conf.h:74
CORE_GROUP_FLAG_ASCII
#define CORE_GROUP_FLAG_ASCII
Definition: core.h:188
data
struct core_data data
Global data object (shared by all threads)
Definition: core.c:242
ENC_URI_SCHEME_MAILTO
Definition: encoding.h:135
ENC_CTS_PLAIN
Definition: encoding.h:39
ui_lock
int ui_lock(void)
Lock for multithread support.
Definition: gui.cxx:12856
core_get_datetime
const char * core_get_datetime(int force_utc)
Get current date and time in RFC 5322 format (exported for UI)
Definition: core.c:5493
core_article_header::x_newsr
const char * x_newsr
Definition: core.h:125
core_article_header::mime_v
const char * mime_v
Definition: core.h:121
CONF_TVIEW
Definition: conf.h:53
ENC_CTS_DIGEST
Definition: encoding.h:42
CONF_PASS
Definition: conf.h:44
enc_convert_octet_to_hex
int enc_convert_octet_to_hex(char *result, unsigned int octet)
Convert octet to hexadecimal (ASCII) format.
Definition: encoding.c:3681
core_destroy_entity_header
void core_destroy_entity_header(struct core_article_header **ehp)
Destructor for MIME multipart entity header object (exported for UI)
Definition: core.c:5366
CONF_USER
Definition: conf.h:43
xdg_get_confdir
const char * xdg_get_confdir(const char *)
Get configuration directory.
Definition: xdg.c:116
CORE_SIG_FLAG_SEPARATOR
#define CORE_SIG_FLAG_SEPARATOR
Definition: core.h:199
conf_ephemeral_passwd
int conf_ephemeral_passwd
Flag indicating that password should not be stored in configfile.
Definition: conf.c:70
CONF_POS_X
Definition: conf.h:32
core_get_article_body
int core_get_article_body(const core_anum_t *id, unsigned int cookie)
Get article body (exported for UI)
Definition: core.c:4428
UI_STATUS
#define UI_STATUS(s)
Replace message in main window status bar.
Definition: gui.cxx:1661
core_article_header::mime_cd
const char * mime_cd
Definition: core.h:124
ENC_CTS_OCTETSTREAM
Definition: encoding.h:44
ENC_CTS_RFC822
Definition: encoding.h:43
ENC_URI_SCHEME_FTP
Definition: encoding.h:133
enc_mime_ct
MIME content type information.
Definition: encoding.h:112
CONF_COLOR_LEVEL4
Definition: conf.h:89
CONF_TILE_X
Definition: conf.h:36
core_get_cancel_key
const char * core_get_cancel_key(unsigned int scheme, const char *msgid)
Create Cancel-Key for Message-ID (exported for UI)
Definition: core.c:5772
enc_convert_posix_to_canonical
const char * enc_convert_posix_to_canonical(const char *s)
Convert from local (POSIX) to canonical (RFC 822) form.
Definition: encoding.c:4679
enc_uri_scheme
enc_uri_scheme
URI schemes.
Definition: encoding.h:129
core_article_header::fup2
const char * fup2
Definition: core.h:115
CORE_HIERARCHY_UPDATE
Definition: core.h:162
core_get_article_by_mid
int core_get_article_by_mid(const char *mid, unsigned int cookie)
Get complete article by Message-ID (exported for UI)
Definition: core.c:4242
core_article_header::date
core_time_t date
Definition: core.h:113
core_range::last
core_anum_t last
Definition: core.h:77
core_check_already_read
int core_check_already_read(struct core_groupstate *group, struct core_hierarchy_element *article)
Check whether article was alread read (exported for UI)
Definition: core.c:4748
enc_convert_iso8601_to_timestamp
int enc_convert_iso8601_to_timestamp(const char **ts, const char *isodate)
Convert ISO 8601 conformant date to canonical timestamp.
Definition: encoding.c:3520
CONF_TILE_Y
Definition: conf.h:37
CONF_CRL_UPD_TS
Definition: conf.h:66
core_get_signature
const char * core_get_signature(unsigned int *warnings)
Get signature for outgoing messages (exported for UI)
Definition: core.c:6044
ENC_CTS_ALTERNATIVE
Definition: encoding.h:41
CONF_INV_ORDER
Definition: conf.h:69
conf::val
union conf_entry_val val
Definition: conf.h:113
enc_uc_repair_utf8
const char * enc_uc_repair_utf8(const char *s)
Repair UTF-8 encoding.
Definition: encoding.c:4159
ext_editor_terminate
void ext_editor_terminate(long int editor_pid)
Terminate external editor.
Definition: extutils.c:653
ext_handler_email
int ext_handler_email(const char *recipient, const char *subject, const char *body)
Send e-mail to single recipient.
Definition: extutils.c:170
CONF_SEARCH_CASE_IS
Definition: conf.h:78
core_data::data
void * data
Definition: core.h:70
core_tmpfile_create
const char * core_tmpfile_create(void)
Create temporary file (exported for UI)
Definition: core.c:6427
CONF_ENABLE_UAGENT
Definition: conf.h:79
core_article_header::lines
unsigned long int lines
Definition: core.h:129
ENC_CT_MESSAGE
Definition: encoding.h:31
ENC_CT_IMAGE
Definition: encoding.h:27
core_get_overview
int core_get_overview(struct core_range *range, unsigned int cookie)
Get article header overview (exported for UI)
Definition: core.c:4173
CONF_QUOTEUNIFY
Definition: conf.h:60
main
int main(int argc, char **argv)
Program entry point.
Definition: main.cxx:317
enc_create_name_addr
const char * enc_create_name_addr(const char *data, size_t offset)
Create a "name-addr" construct according to RFC 5322.
Definition: encoding.c:2969
enc_mime_get_cd
void enc_mime_get_cd(const char *hf_body, enum enc_mime_cd *type, const char **filename)
Decode content disposition.
Definition: encoding.c:6605
CONF_ENC
Definition: conf.h:40
core_get_article_header
int core_get_article_header(const core_anum_t *id, unsigned int cookie)
Get article header (exported for UI)
Definition: core.c:4366
ext_editor
int ext_editor(const char *tfpn, int async,...)
Start external editor for article composition.
Definition: extutils.c:517
core_update_subscribed_group_states
int core_update_subscribed_group_states(size_t *num, struct core_groupstate **list, size_t *index)
Get states of subscribed groups (exported for UI)
Definition: core.c:3589
enc_mime_ct::flags
unsigned int flags
Definition: encoding.h:118
ENC_CS_UTF_8
Definition: encoding.h:99
core_extract_groups
const char ** core_extract_groups(const char *body)
Extract groups from 'Newsgroups' header field (exported for UI)
Definition: core.c:3292
enc_mime_cd
enc_mime_cd
IDs for supported MIME content disposition.
Definition: encoding.h:104
core_disconnect
void core_disconnect(void)
Close nexus (exported for UI)
Definition: core.c:6555
core_article_header::x_pagent
const char * x_pagent
Definition: core.h:127
CONF_SERVER
Definition: conf.h:38
UI_STATIC_STYLE_BUFSIZE
#define UI_STATIC_STYLE_BUFSIZE
Static buffer size for compose window style update callback.
Definition: gui.cxx:202
filter_match_reply_to_own
int filter_match_reply_to_own(const struct core_hierarchy_element *he)
Check for reply to own article.
Definition: filter.c:1643
CONF_FQDN
Definition: conf.h:48
core_get_article
int core_get_article(const core_anum_t *id, unsigned int cookie)
Get complete article (exported for UI)
Definition: core.c:4304
enc_mime_multipart
size_t enc_mime_multipart(const char *s, const char *b, struct enc_mime_mpe **mpe)
Parse MIME multipart content.
Definition: encoding.c:7198
enc_mime_word_encode
int enc_mime_word_encode(const char **r, const char *b, size_t pl)
Encode header field body using MIME encoded-word tokens.
Definition: encoding.c:5089
enc_mime_get_ct
void enc_mime_get_ct(struct enc_mime_ct *ct, const char *hf_body, char *bo)
Decode MIME "Content-Type" header field.
Definition: encoding.c:6221
core_entity_parser
const char * core_entity_parser(const char *entity, size_t len, struct core_article_header **e_h, size_t *e_len)
Parse header of MIME multipart entity (exported for UI)
Definition: core.c:5302
core_save_to_file
int core_save_to_file(const char *pathname, const char *s)
Save string to text file (exported for UI)
Definition: core.c:6382
tls_crl_update_control
void tls_crl_update_control(int)
Enable or disable automatic CRL updates.
Definition: tls.c:2873
CONF_TESTGRP_KWORDS
Definition: conf.h:62
CONF_SIZE_Y
Definition: conf.h:35
enc_mime_cte
enc_mime_cte
IDs for supported MIME content transfer encodings.
Definition: encoding.h:48
core_article_header::msgid
const char * msgid
Definition: core.h:109
CORE_SIG_FLAG_LENGTH
#define CORE_SIG_FLAG_LENGTH
Definition: core.h:202
ENC_CT_TEXT
Definition: encoding.h:26
core_get_introduction
const char * core_get_introduction(const char *ca, const char *ngl)
Get introduction line for citation (exported for UI)
Definition: core.c:6164
ui_exit
void ui_exit(void)
Shutdown GUI.
Definition: gui.cxx:12677
ext_free
void ext_free(void *p)
Free an object allocated by external program delegation module.
Definition: extutils.c:1160
print_error
void print_error(const char *)
Print error message.
Definition: main.cxx:276
ui_init
void ui_init(int argc, char **argv)
Init GUI.
Definition: gui.cxx:12289
core_set_group
int core_set_group(const char *name, unsigned int cookie)
Set current group (exported for UI)
Definition: core.c:3771

Generated at 2026-01-27 using  doxygen