gui.cxx
Go to the documentation of this file.
1 // ===========================================================================
2 //! \file
3 //! \brief GUI based on FLTK 1.3
4 //!
5 //! Copyright (c) 2012-2024 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_Hold_Browser.H>
45 #include <FL/Fl_File_Chooser.H>
46 #include <FL/Fl_Group.H>
47 #include <FL/Fl_Input.H>
48 #include <FL/Fl_Input_Choice.H>
49 #include <FL/Fl_Menu_.H>
50 #include <FL/Fl_Menu_Bar.H>
51 #include <FL/Fl_Menu_Item.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 
75 // Local
76 extern "C"
77 {
78 #include "bdate.h"
79 #include "conf.h"
80 #include "core.h"
81 #include "encoding.h"
82 #include "extutils.h"
83 #include "filter.h"
84 #include "log.h"
85 #include "nls.h"
86 #include "xdg.h"
87 }
88 #include "main.hxx"
89 #include "tls.hxx"
90 #include "ui.hxx"
91 
92 
93 // =============================================================================
94 //! \defgroup GUI GUI: Graphical User Interface
95 //!
96 //! This graphical user interface is based on FLTK 1.3 and is intended to work
97 //! with a FLTK 1.3.0 shared library. Using features that are added later is not
98 //! allowed (without ABI guards) to preserve backward compatibility.
99 //!
100 //! \note
101 //! If you hear bugging beeps when information windows appear, this file is the
102 //! wrong place to fix. These beeps are created by FLTK internally and can be
103 //! eliminated by patching the file 'fltk-1.3.0/src/fl_ask.cxx'. Commenting out
104 //! all calls to 'fl_beep()' and rebuilding FLTK will give you comfortable
105 //! silence. Newer FLTK versions already contains this "silence patch".
106 //! @{
107 
108 
109 // =============================================================================
110 // Experimental features
111 
112 //! Setting this to 1 enables line count in article tree/list (EXPERIMENTAL)
113 #define USE_LINE_COUNT 0
114 
115 //! Setting this to 1 enables article number in article tree/list (EXPERIMENTAL)
116 #define USE_ARTICLE_NUMBER 0
117 
118 
119 // =============================================================================
120 // Constants
121 
122 //! \brief Message prefix for MAIN module
123 #define MAIN_ERR_PREFIX "GUI: "
124 
125 //! \name Fixed widget colors
126 //!
127 //! Override some FLTK defaults that can't be configured via X resources.
128 //! @{
129 //! Selection color for menu
130 #define UI_COLOR_MENU_SELECTION (Fl_Color) 0x50505000UL
131 //! Color for progress bar
132 #define UI_COLOR_PROGRESS_BAR (Fl_Color) 0x50505000UL
133 //! Color for selected radio button
134 #define UI_COLOR_RADIO_BUTTON (Fl_Color) 0x50505000UL
135 //! @}
136 
137 //! Number of styles for article content syntax highlighting
138 #define UI_STYLES_LEN 9
139 
140 //! \name Callback action controls
141 //! @{
142 #define UI_CB_START 0
143 #define UI_CB_CONTINUE 1
144 #define UI_CB_FINISH 2
145 //! @}
146 
147 //! \name Callback cookies
148 //! @{
149 #define UI_CB_COOKIE_SERVER 0U
150 #define UI_CB_COOKIE_GROUPLIST 1U
151 #define UI_CB_COOKIE_GROUPLABELS 2U
152 #define UI_CB_COOKIE_GROUPPROPOSAL 3U
153 #define UI_CB_COOKIE_GROUPINFO1 4U
154 #define UI_CB_COOKIE_GROUPINFO2 5U
155 #define UI_CB_COOKIE_GROUP 6U
156 #define UI_CB_COOKIE_OVERVIEW 7U
157 #define UI_CB_COOKIE_HEADER 8U
158 #define UI_CB_COOKIE_BODY 9U
159 #define UI_CB_COOKIE_MOTD 10U
160 #define UI_CB_COOKIE_ARTICLE 11U
161 #define UI_CB_COOKIE_SRC 12U
162 #define UI_CB_COOKIE_POST 13U
163 //! @}
164 
165 //! \name Encryption algorithms
166 //!
167 //! All nonzero values additionally provide server to client authentication via
168 //! X509 certificate.
169 //! @{
170 #define UI_ENC_NONE 0 // No encryption
171 #define UI_ENC_STRONG 1 // TLS with strong encryption and forward secrecy
172 #define UI_ENC_WEAK 2 // TLS compatibility mode offering weak cipher suites
173 //! @}
174 
175 //! \name Authentication algorithms (client to server)
176 //! @{
177 #define UI_AUTH_NONE 0 // No authentication
178 #define UI_AUTH_USER 1 // AUTHINFO USER/PASS as defined in RFC 4643
179 //! @}
180 
181 //! \name Limits for clamp article count (CONF_CAC) configuration value
182 //! @{
183 #define UI_CAC_MIN 10 // Lower limit: 1
184 #define UI_CAC_MAX 50000 // Upper limit: INT_MAX
185 //! @}
186 
187 //! Size of buffer for header field creation
188 #define UI_HDR_BUFSIZE (std::size_t) 998
189 
190 //! Static buffer size for compose window style update callback
191 #define UI_STATIC_STYLE_BUFSIZE (std::size_t) 80
192 
193 //! Maximum number of groups for crossposting
194 #define UI_XPOST_LIMIT (std::size_t) 10
195 
196 //! Enable references line in ArticleWindow class
197 //!
198 //! This is currently not useful, because ArticleWindow has no hyperlink
199 //! support.
200 #define UI_AW_REFERENCES 0
201 
202 // Use window class with or without double buffering
203 #if CFG_DB_DISABLE
204 # define UI_WINDOW_CLASS Fl_Window
205 #else // CFG_DB_DISABLE
206 # define UI_WINDOW_CLASS Fl_Double_Window
207 #endif // CFG_DB_DISABLE
208 
209 // Enable window icons on X11 if possible
210 #define USE_WINDOW_ICON 0
211 #ifdef FL_ABI_VERSION
212 # if 10303 <= FL_ABI_VERSION
213  // At least FLTK 1.3.3 is required
214 # undef USE_WINDOW_ICON
215 # define USE_WINDOW_ICON 1
216 # endif // 10303 <= FL_ABI_VERSION
217 #endif // FL_ABI_VERSION
218 
219 
220 // =============================================================================
221 // Data types
222 
223 // Target position for scrolling
224 enum ui_scroll
225 {
226  UI_SCROLL_NONE,
227  UI_SCROLL_TOP,
228  UI_SCROLL_MIDDLE,
229  UI_SCROLL_BOTTOM
230 };
231 
232 // Server configuration
233 class ServerConfig
234 {
235  public:
236  char server[128];
237  char service[6];
238  int enc; // Use UI_ENC_xxx constants
239  int auth; // Use UI_AUTH_xxx constants
240 
241  inline void serverReplace(const char* s)
242  {
243  std::strncpy(server, s, 128); server[127] = 0;
244  }
245  inline void serviceReplace(const char* s)
246  {
247  std::strncpy(service, s, 6); service[5] = 0;
248  }
249  inline void serviceReplace(unsigned int i)
250  {
251  std::ostringstream ss;
252 
253  if(0xFFFFU < i) { i = 0xFFFFU; }
254  ss << i << std::flush;
255  const std::string& s = ss.str();
256  std::strncpy(service, s.c_str(), 6); service[5] = 0;
257  }
258 };
259 
260 // MIME multipart content
261 class MIMEContent
262 {
263  struct MIMEContentListElement
264  {
265  enc_mime_cte cte;
266  enc_mime_ct ct;
267  const char* header;
268  const char* content;
269  enum enc_mime_cd type; // Content-Disposition
270  const char* filename; // From Content-Disposition "filename" parameter
271  MIMEContentListElement* next;
272  };
273 
274  private:
275  bool multipart; // Multipart flag (to enable display of entity headers)
276  std::size_t partNum; // Number of elements in linked list
277  MIMEContentListElement* partList; // Linked list
278 
279  const char* createMessageHeader(const char*, std::size_t);
280  MIMEContentListElement* decodeElement(const char*, std::size_t, char*,
281  bool, bool, std::size_t*);
282  MIMEContentListElement* initElement(const char*, std::size_t,
284  struct core_article_header*);
285  public:
286  inline bool is_multipart(void) { return(multipart); }
287  inline std::size_t parts(void) { return(partNum); }
288  inline const char* part_header(std::size_t i)
289  {
290  MIMEContentListElement* mcle = partList;
291 
292  if(!partNum || i >= partNum) { return(NULL); }
293  else
294  {
295  while(i--) { mcle = mcle->next; }
296  return(mcle->header);
297  }
298  }
299  inline const char* part(std::size_t i, enc_mime_cte* te,
300  enc_mime_ct** ctp)
301  {
302  MIMEContentListElement* mcle = partList;
303 
304  if(!partNum || i >= partNum) { return(NULL); }
305  else
306  {
307  while(i--) { mcle = mcle->next; }
308  *te = mcle->cte;
309  if(NULL != ctp) { *ctp = &mcle->ct; }
310  return(mcle->content);
311  }
312  }
313  inline enum enc_mime_cd type(std::size_t i)
314  {
315  MIMEContentListElement* mcle = partList;
316 
317  if(!partNum || i >= partNum) { return(ENC_CD_UNKNOWN); }
318  else
319  {
320  while(i--) { mcle = mcle->next; }
321  return(mcle->type);
322  }
323  }
324  inline const char* filename(std::size_t i)
325  {
326  MIMEContentListElement* mcle = partList;
327 
328  if(!partNum || i >= partNum) { return(NULL); }
329  else
330  {
331  while(i--) { mcle = mcle->next; }
332  return(mcle->filename);
333  }
334  }
335  MIMEContent(struct core_article_header*, const char*);
336  ~MIMEContent(void);
337 };
338 
339 // A clone of Fl_Text_Display
340 class My_Text_Display : public Fl_Text_Display
341 {
342  private:
343  int linkPushed;
344 
345  int handle(int);
346  public:
347  My_Text_Display(int X, int Y, int W, int H, const char* L = 0)
348  : Fl_Text_Display(X, Y, W, H, L) { linkPushed = -1; }
349 };
350 
351 // A clone of Fl_Tree
352 class My_Tree : public Fl_Tree
353 {
354  private:
355  bool updated; // Used to block article selection while updating
356  bool positions_recalculated;
357  Fl_Tree_Item* current_article;
358 
359  int handle(int);
360  public:
361  inline void update_in_progress(bool state) { updated = !state; }
362  inline bool update_in_progress(void) { return !updated; }
363  inline void not_drawn(void) { positions_recalculated = false; }
364  inline bool drawn(void)
365  {
366  // Workaround for "infinite busy" without drawing area
367  if (0 == w() || 0 == h()) { return true; }
368  return(positions_recalculated);
369  }
370  inline void draw(void)
371  {
372  // This will calculate the positions of the tree items
373  Fl_Tree::draw();
374  positions_recalculated = true;
375  }
376  inline void store_current(Fl_Tree_Item* ti)
377  {
378  current_article = ti;
379  }
380  inline void select_former(void)
381  {
382  if(NULL != current_article)
383  {
384  set_item_focus(current_article);
385  select_only(current_article, 0);
386  }
387  }
388  My_Tree(int X, int Y, int W, int H, const char* L = 0)
389  : Fl_Tree(X, Y, W, H, L)
390  {
391  updated = true;
392  positions_recalculated = false;
393  current_article = NULL;
394  }
395 };
396 
397 // Server configuration window
398 class ServerCfgWindow : public UI_WINDOW_CLASS
399 {
400  private:
401  ServerConfig* sconf;
402  int finished;
403  Fl_Group* grpAuth;
404  Fl_Group* scfgGroup;
405  Fl_Input* scfgHostname;
406  Fl_Input* scfgService;
407  Fl_Radio_Round_Button* scfgTlsOff;
408  Fl_Radio_Round_Button* scfgTlsStrong;
409  Fl_Radio_Round_Button* scfgTlsWeak;
410  Fl_Radio_Round_Button* scfgAuthOff;
411  Fl_Radio_Round_Button* scfgAuthUser;
412  char* hostname;
413  char* service;
414  char* tls_headline;
415 
416  // OK callback
417  inline void ok_cb_i(void) { finished = 1; }
418  static void ok_cb(Fl_Widget*, void* w)
419  {
420  ((ServerCfgWindow*) w)->ok_cb_i();
421  }
422 
423  // Cancel callback
424  inline void cancel_cb_i(void) { finished = -1; }
425  static void cancel_cb(Fl_Widget*, void* w)
426  {
427  ((ServerCfgWindow*) w)->cancel_cb_i();
428  }
429 
430  // Encryption radio buttons callback
431  inline void enc_cb_i(bool enc)
432  {
433  if(!enc)
434  {
435  scfgService->value("nntp");
436 #if !CFG_NNTP_AUTH_UNENCRYPTED
437  scfgAuthOff->setonly();
438  grpAuth->deactivate();
439 #endif
440  }
441  else
442  {
443  scfgService->value("nntps");
444  grpAuth->activate();
445  }
446  }
447  static void enc_off_cb(Fl_Widget*, void* w)
448  {
449  ((ServerCfgWindow*) w)->enc_cb_i(false);
450  }
451  static void enc_on_cb(Fl_Widget*, void* w)
452  {
453  ((ServerCfgWindow*) w)->enc_cb_i(true);
454  }
455 
456  public:
457  int process(void);
458  ServerCfgWindow(ServerConfig*, const char*);
459  ~ServerCfgWindow(void);
460 };
461 
462 // Identity configuration window
463 class IdentityCfgWindow : public UI_WINDOW_CLASS
464 {
465  private:
466  Fl_Group* cfgGroup;
467  Fl_Input* fromName;
468  Fl_Input* fromEmail;
469  Fl_Input* replytoName;
470  Fl_Input* replytoEmail;
471 
472  // OK callback
473  inline void ok_cb_i(void);
474  static void ok_cb(Fl_Widget*, void* w)
475  {
476  ((IdentityCfgWindow*) w)->ok_cb_i();
477  }
478 
479  // Cancel callback
480  static void cancel_cb(Fl_Widget*, void* w)
481  {
482  Fl::delete_widget((Fl_Widget*) w);
483  }
484  public:
485  IdentityCfgWindow(const char*);
486  ~IdentityCfgWindow(void);
487 };
488 
489 // Misc configuration window
490 class MiscCfgWindow : public UI_WINDOW_CLASS
491 {
492  private:
493  Fl_Tabs* cfgTabs;
494  Fl_Group* cacGroup;
495  Fl_Input* cacField;
496  Fl_Check_Button* cmprEnable;
497  Fl_Check_Button* localTime;
498  Fl_Check_Button* uagentEnable;
499  Fl_Group* qsGroup;
500  Fl_Check_Button* qsSpace;
501  Fl_Check_Button* qsUnify;
502 
503  // OK callback
504  inline void ok_cb_i(void);
505  static void ok_cb(Fl_Widget*, void* w)
506  {
507  ((MiscCfgWindow*) w)->ok_cb_i();
508  }
509 
510  // Cancel callback
511  static void cancel_cb(Fl_Widget*, void* w)
512  {
513  Fl::delete_widget((Fl_Widget*) w);
514  }
515  public:
516  MiscCfgWindow(const char*);
517  ~MiscCfgWindow(void);
518 };
519 
520 
521 // Search window
522 class SearchWindow : public UI_WINDOW_CLASS
523 {
524  private:
525  Fl_Input* searchField;
526  Fl_Check_Button* cisEnable;
527  const char** currentSearchString;
528 
529  // OK callback
530  inline void ok_cb_i(void);
531  static void ok_cb(Fl_Widget*, void* w)
532  {
533  ((SearchWindow*) w)->ok_cb_i();
534  ((SearchWindow*) w)->finished = 1;
535  }
536 
537  // Cancel callback
538  static void cancel_cb(Fl_Widget*, void* w)
539  {
540  ((SearchWindow*) w)->finished = -1;
541  }
542  public:
543  int finished;
544  SearchWindow(const char*, const char**);
545  ~SearchWindow(void);
546 };
547 
548 
549 // Protocol console window
550 class ProtocolConsole : public UI_WINDOW_CLASS
551 {
552  public:
553  Fl_Text_Display* consoleDisplay;
554  private:
555  Fl_Text_Buffer* consoleText;
556  FILE* logfp;
557  int nolog;
558 
559  // Exit callback
560  static void exit_cb(Fl_Widget*, void* w)
561  {
562  Fl::delete_widget((Fl_Widget*) w);
563  }
564 
565  public:
566  void update(void);
567  ProtocolConsole(const char*);
568  ~ProtocolConsole(void);
569 };
570 
571 // Subscribe window
572 class SubscribeWindow : public UI_WINDOW_CLASS
573 {
574  private:
575  Fl_Group* subscribeGroup;
576  Fl_Tree* subscribeTree;
577  core_groupdesc* grouplist;
578  core_grouplabel* grouplabels;
579 
580  // OK callback
581  inline void ok_cb_i(void);
582  static void ok_cb(Fl_Widget*, void* w)
583  {
584  ((SubscribeWindow*) w)->ok_cb_i();
585  }
586 
587  // Cancel callback
588  static void cancel_cb(Fl_Widget*, void* w)
589  {
590  Fl::delete_widget((Fl_Widget*) w);
591  }
592 
593  // Tree callback
594  inline void tree_cb_i(void)
595  {
596  Fl_Tree_Item* ti;
597 
598  if(FL_TREE_REASON_SELECTED == subscribeTree->callback_reason())
599  {
600  // Immediately reset selection again if item is not selectable
601  ti = subscribeTree->callback_item();
602  if(NULL == ti->user_data()) { subscribeTree->deselect(ti, 0); }
603  }
604  }
605  static void tree_cb(Fl_Widget*, void* w)
606  {
607  ((SubscribeWindow*) w)->tree_cb_i();
608  }
609  public:
610  inline void add(const char* entry, core_anum_t num, const char* label)
611  {
612  Fl_Tree_Item* ti;
613  std::ostringstream labelString;
614 
615  ti = subscribeTree->find_item(entry);
616  if(NULL == ti) { ti = subscribeTree->add(entry); }
617  if(NULL == ti)
618  {
619  PRINT_ERROR("Adding group to subscription tree failed");
620  }
621  else
622  {
623  // Set all selectable items to bold font and tag them with user data
624  ti->labelfont(FL_HELVETICA_BOLD);
625  ti->user_data((void*) entry);
626  labelString << ti->label() << " (" << num << ")";
627  if(NULL != label) { labelString << " | " << label; }
628  // Attention:
629  //
630  // const char* ls = labelString.str().c_str()
631  //
632  // creates 'ls' with undefined value because the compiler is
633  // allowed to free the temporary string object returned by
634  // 'str()' immediately after the assignment!
635  // Assigning names to temporary string objects forces them to
636  // stay in memory as long as their names go out of scope (this
637  // is what we need).
638  const std::string& ls = labelString.str();
639  ti->label(ls.c_str());
640  }
641  }
642  inline void collapseAll(void)
643  {
644  Fl_Tree_Item* i;
645 
646  for(i = subscribeTree->first(); i; i = subscribeTree->next(i))
647  {
648  subscribeTree->close(i, 0);
649  }
650  subscribeTree->open(subscribeTree->root(), 0);
651  }
652  SubscribeWindow(const char* label, core_groupdesc* glist,
653  core_grouplabel* labels);
654  ~SubscribeWindow(void);
655 };
656 
657 // MID search window
658 class MIDSearchWindow : public UI_WINDOW_CLASS
659 {
660  private:
661  Fl_Group* cfgGroup;
662  Fl_Input* mid;
663 
664  // OK callback
665  inline void ok_cb_i(void);
666  static void ok_cb(Fl_Widget*, void* w)
667  {
668  ((MIDSearchWindow*) w)->ok_cb_i();
669  }
670 
671  // Cancel callback
672  static void cancel_cb(Fl_Widget*, void* w)
673  {
674  Fl::delete_widget((Fl_Widget*) w);
675  }
676  public:
677  MIDSearchWindow(const char*);
678  ~MIDSearchWindow(void);
679 };
680 
681 // Bug report window
682 class BugreportWindow : public UI_WINDOW_CLASS
683 {
684  private:
685  Fl_Text_Display* bugreportDisplay;
686  Fl_Text_Buffer* bugreportText;
687 
688  // Exit callback
689  static void exit_cb(Fl_Widget*, void* w)
690  {
691  Fl::delete_widget((Fl_Widget*) w);
692  }
693 
694  public:
695  BugreportWindow(const char*, const char*);
696  ~BugreportWindow(void);
697 };
698 
699 // License window
700 class LicenseWindow : public UI_WINDOW_CLASS
701 {
702  private:
703  Fl_Text_Display* licenseDisplay;
704  Fl_Text_Buffer* licenseText;
705 
706  // Exit callback
707  static void exit_cb(Fl_Widget*, void* w)
708  {
709  Fl::delete_widget((Fl_Widget*) w);
710  }
711 
712  public:
713  LicenseWindow(const char*);
714  ~LicenseWindow(void);
715 };
716 
717 // Message of the day window
718 class MotdWindow : public UI_WINDOW_CLASS
719 {
720  private:
721  Fl_Text_Display* motdDisplay;
722  Fl_Text_Buffer* motdText;
723 
724  // Exit callback
725  static void exit_cb(Fl_Widget*, void* w)
726  {
727  Fl::delete_widget((Fl_Widget*) w);
728  }
729 
730  public:
731  MotdWindow(const char*, const char*);
732  ~MotdWindow(void);
733 };
734 
735 // Article window
736 class ArticleWindow : public UI_WINDOW_CLASS
737 {
738  private:
739  Fl_Group* articleGroup;
740  My_Text_Display* articleDisplay;
741  Fl_Text_Buffer* articleText;
742  Fl_Text_Buffer* articleStyle;
743  Fl_Text_Display::Style_Table_Entry* styles;
744  int styles_len;
745  MIMEContent* mimeData;
746  struct core_hierarchy_element* alt_hier;
747 
748  const char* printHeaderFields(struct core_article_header*);
749  void articleUpdate(Fl_Text_Buffer* article);
750 
751  // Exit callback
752  static void cancel_cb(Fl_Widget*, void* w)
753  {
754  Fl::delete_widget((Fl_Widget*) w);
755  }
756 
757  public:
758  ArticleWindow(const char*, const char*);
759  ~ArticleWindow(void);
760 };
761 
762 // Article source window
763 class ArticleSrcWindow : public UI_WINDOW_CLASS
764 {
765  private:
766  Fl_Group* srcGroup;
767  Fl_Text_Display* srcDisplay;
768  Fl_Text_Buffer* srcText;
769  Fl_Text_Buffer* srcStyle;
770  Fl_Text_Display::Style_Table_Entry* styles;
771  char* srcArticle;
772  const char* pathname;
773 
774  // Save callback
775  inline void save_cb_i(void);
776  static void save_cb(Fl_Widget*, void* w)
777  {
778  ((ArticleSrcWindow*) w)->save_cb_i();
779  }
780 
781  // Exit callback
782  static void cancel_cb(Fl_Widget*, void* w)
783  {
784  Fl::delete_widget((Fl_Widget*) w);
785  }
786 
787  public:
788  ArticleSrcWindow(const char*, const char*);
789  ~ArticleSrcWindow(void);
790 };
791 
792 // Compose window
793 class ComposeWindow : public UI_WINDOW_CLASS
794 {
795  private:
796  Fl_Tabs* compTabs;
797  Fl_Group* subjectGroup;
798  Fl_Input* subjectField;
799  Fl_Group* compGroup;
800  Fl_Group* uriEncGroup;
801  Fl_Group* advancedGroup;
802  Fl_Text_Buffer* compHeader;
803  Fl_Text_Display::Style_Table_Entry* styles;
804  Fl_Text_Buffer* currentStyle;
805  Fl_Text_Buffer* compText;
806  // On URI decoder tab
807  Fl_Box* uriHeaderField;
808  Fl_Input_Choice* uriSchemeField;
809  Fl_Input* uriBodyField;
810  // On advanced tab
811  Fl_Input* newsgroupsField;
812  Fl_Input_Choice* fup2Field;
813  Fl_Input_Choice* keywordField;
814  Fl_Input* expireField;
815  Fl_Input_Choice* distriField;
816  Fl_Check_Button* archiveButton;
817  Fl_Box* fillSpace;
818 
819  // Change subject callback
820  inline void change_cb_i(Fl_Widget* w);
821  static void change_cb(Fl_Widget*, void* w)
822  {
823  ((ComposeWindow*) w)->change_cb_i((Fl_Widget*) w);
824  }
825 
826  // Style update callback
827  inline void style_update_cb_i(int pos, int nInserted, int nDeleted,
828  int nRestyled, const char* deletedText,
829  Fl_Text_Buffer* style,
830  Fl_Text_Editor* editor);
831  static void style_update_cb(int pos, int nInserted, int nDeleted,
832  int nRestyled, const char* deletedText,
833  void* w)
834  {
835  ((ComposeWindow*) w)
836  ->style_update_cb_i(pos, nInserted, nDeleted, nRestyled, deletedText,
837  ((ComposeWindow*) w)->currentStyle,
838  ((ComposeWindow*) w)->compEditor);
839 
840  }
841 
842  // Send callback
843  inline void send_cb_i(Fl_Widget* w);
844  static void send_cb(Fl_Widget*, void* w)
845  {
846  ((ComposeWindow*) w)->send_cb_i((Fl_Widget*) w);
847  }
848 
849  // Cancel callback
850  inline void cancel_cb_i(Fl_Widget*);
851  static void cancel_cb(Fl_Widget*, void* w)
852  {
853  ((ComposeWindow*) w)->cancel_cb_i((Fl_Widget*) w);
854  }
855 
856  // URI insert callback
857  inline void uri_insert_cb_i(Fl_Widget*);
858  static void uri_insert_cb(Fl_Widget*, void* w)
859  {
860  ((ComposeWindow*) w)->uri_insert_cb_i((Fl_Widget*) w);
861  }
862 
863  int searchHeaderField(const char*, int*, int*);
864  const char* extractHeaderField(const char*);
865  int replaceHeaderField(const char*, const char*);
866  void deleteHeaderField(const char*);
867  int checkArticleBody(const char*);
868  public:
869  Fl_Text_Editor* compEditor;
870  ComposeWindow(const char*, const char*, const char*, const char*,
871  struct core_article_header*, bool);
872  ~ComposeWindow(void);
873 };
874 
875 // Main window
876 class MainWindow : public UI_WINDOW_CLASS
877 {
878  // States for the callback locking state machine
879  enum mainWindowState
880  {
881  STATE_READY,
882  STATE_MUTEX, // Allow nothing else to run in parallel
883  STATE_SERVER1,
884  STATE_SERVER2,
885  STATE_PROPOSAL,
886  STATE_GROUP,
887  STATE_SCROLL,
888  STATE_NEXT,
889  STATE_COMPOSE,
890  STATE_POST
891  };
892 
893  // Events for the callback locking state machine
894  enum mainWindowEvent
895  {
896  // Server configuration
897  EVENT_SERVER,
898  EVENT_SERVER_EXIT,
899  // Group subscription
900  EVENT_SUBSCRIBE,
901  EVENT_SUBSCRIBE_EXIT,
902  // Group list proposals
903  EVENT_GL_PROPOSAL,
904  EVENT_GL_PROPOSAL_EXIT,
905  // Group list refresh
906  EVENT_GL_REFRESH,
907  EVENT_GL_REFRESH_EXIT,
908  // Group selection
909  EVENT_G_SELECT,
910  EVENT_G_SELECT_EXIT,
911  // Article tree refresh
912  EVENT_AT_REFRESH,
913  EVENT_AT_REFRESH_EXIT,
914  // Prepare for article selection
915  EVENT_A_PREPARE,
916  // Article selection
917  EVENT_A_SELECT,
918  EVENT_A_SELECT_EXIT,
919  // View article (not from current group)
920  EVENT_A_VIEW,
921  EVENT_A_VIEW_EXIT,
922  // Mark articles read (current group)
923  EVENT_A_MAAR,
924  EVENT_A_MAAR_EXIT,
925  // Mark articles read (all groups)
926  EVENT_A_MAGAR,
927  EVENT_A_MAGAR_EXIT,
928  // View source code
929  EVENT_SRC_VIEW,
930  EVENT_SRC_VIEW_EXIT,
931  // View message of the day
932  EVENT_MOTD_VIEW,
933  EVENT_MOTD_VIEW_EXIT,
934  // Scroll down or (at the end) select next unread article
935  EVENT_SCROLL_NEXT,
936  EVENT_SCROLL_NEXT_EXIT,
937  // Article composition
938  EVENT_COMPOSE,
939  EVENT_COMPOSE_EXIT,
940  // Article posting
941  EVENT_POST,
942  EVENT_POST_EXIT
943  };
944 
945  public:
946  Fl_Box* statusBar;
947  Fl_Progress* progressBar;
948  std::ostringstream aboutString;
949  ComposeWindow* composeWindow;
950  int composeWindowLock;
951  unsigned int hyperlinkStyle;
952  int hyperlinkPosition;
953  private:
954  bool startup; // Flag to preserve greeting message on startup
955  bool busy; // Mouse cursor state
956  bool unsub; // Flag to indicate current group was unsubscribed
957  mainWindowState mainState;
958  SubscribeWindow* subscribeWindow;
959  Fl_Tile* contentGroup;
960  Fl_Tile* contentGroup2;
961 #if CFG_COCOA_SYS_MENUBAR
962  Fl_Sys_Menu_Bar* menu;
963 #else // CFG_COCOA_SYS_MENUBAR
964  Fl_Menu_Bar* menu;
965 #endif // CFG_COCOA_SYS_MENUBAR
966  std::size_t groupcount;
967  core_groupdesc* grouplist;
968  Fl_Hold_Browser* groupList;
969  core_groupdesc* subscribedGroups; // Array of subscribed groups
970  core_groupdesc* currentGroup; // Descriptor of current group
971  My_Tree* articleTree;
972  core_hierarchy_element* currentArticleHE;
973  core_hierarchy_element* lastArticleHE;
974  My_Text_Display* text;
975  int wrapMode;
976  Fl_Text_Display::Style_Table_Entry* styles;
977  int styles_len;
978  Fl_Text_Buffer* currentStyle;
979  Fl_Text_Buffer* currentArticle;
980  int currentLine;
981  const char* currentSearchString;
982  int currentSearchPosition;
983  int state; // -1: Abort, 0: Ready, 1: Finished, 2: Empty
984  core_anum_t ai; // Article ID (watermark)
985  core_range ai_range; // Article ID (watermark) range
986  int groupSelect_cb_state;
987  int groupRefresh_cb_state;
988  float progress_percent_value;
989  char progress_percent_label[5];
990  bool progress_skip_update;
991  MIMEContent* mimeData;
992  char* mid_a;
993  bool rot13; // ROT13 applied. Recovery requires reload of article
994 
995  // The group states are loaded/stored from/to the groupfile
996  // Note:
997  // The 'name' fields in the group descriptor array 'subscribedGroups'
998  // point to the memory of the corresponding 'name' fields of 'group_list'.
999  core_groupstate* group_list; // Array of group states
1000  std::size_t group_num; // Number of elements in group state array
1001  std::size_t group_list_index; // Index of current group in 'group_list'
1002  int group_old; // Previous selected value in 'groupList' widget
1003  int group_new; // Selected value in 'groupList' widget
1004 
1005  // Exit callback
1006  inline void progress_release_cb_i(void)
1007  {
1008  progress_skip_update = false;
1009  }
1010  static void progress_release_cb(void* w)
1011  {
1012  ((MainWindow*) w)->progress_release_cb_i();
1013  }
1014 
1015  // Exit callback
1016  inline void exit_cb_i(void);
1017  static void exit_cb(Fl_Widget*, void* w)
1018  {
1019  ((MainWindow*) w)->exit_cb_i();
1020  }
1021 
1022  // Print cooked article callback
1023  inline void print_cb_i(void);
1024  static void print_cb(Fl_Widget*, void* w)
1025  {
1026  ((MainWindow*) w)->print_cb_i();
1027  }
1028 
1029  // Save cooked article (to file) callback
1030  inline void asave_cb_i(void);
1031  static void asave_cb(Fl_Widget*, void* w)
1032  {
1033  ((MainWindow*) w)->asave_cb_i();
1034  }
1035 
1036  // Server callback
1037  inline void server_cb_i(void)
1038  {
1039  updateServer(UI_CB_START);
1040  }
1041  static void server_cb(Fl_Widget*, void* w)
1042  {
1043  ((MainWindow*) w)->server_cb_i();
1044  }
1045 
1046  // Configuration callback
1047  inline void config_cb_i(void);
1048  static void config_cb(Fl_Widget*, void* w)
1049  {
1050  ((MainWindow*) w)->config_cb_i();
1051  }
1052 
1053  // Identity callback
1054  inline void identity_cb_i(void);
1055  static void identity_cb(Fl_Widget*, void* w)
1056  {
1057  ((MainWindow*) w)->identity_cb_i();
1058  }
1059 
1060  // About information callback
1061  inline void about_cb_i(void);
1062  static void about_cb(Fl_Widget*, void* w)
1063  {
1064  ((MainWindow*) w)->about_cb_i();
1065  }
1066 
1067  // View message of the day callback
1068  inline void viewmotd_cb_i(void)
1069  {
1070  viewMotd(UI_CB_START);
1071  }
1072  static void viewmotd_cb(Fl_Widget*, void* w)
1073  {
1074  ((MainWindow*) w)->viewmotd_cb_i();
1075  }
1076 
1077  // MID search callback
1078  inline void mid_search_cb_i(void);
1079  static void mid_search_cb(Fl_Widget*, void* w)
1080  {
1081  ((MainWindow*) w)->mid_search_cb_i();
1082  }
1083 
1084  // Bug callback
1085  inline void bug_cb_i(void);
1086  static void bug_cb(Fl_Widget*, void* w)
1087  {
1088  ((MainWindow*) w)->bug_cb_i();
1089  }
1090 
1091  // License information callback
1092  inline void license_cb_i(void);
1093  static void license_cb(Fl_Widget*, void* w)
1094  {
1095  ((MainWindow*) w)->license_cb_i();
1096  }
1097 
1098  // Protocol console callback
1099  inline void console_cb_i(void);
1100  static void console_cb(Fl_Widget*, void* w)
1101  {
1102  ((MainWindow*) w)->console_cb_i();
1103  }
1104 
1105  // Subscribe callback
1106  inline void gsubscribe_cb_i(void)
1107  {
1108  groupSubscribe(UI_CB_START);
1109  }
1110  static void gsubscribe_cb(Fl_Widget*, void* w)
1111  {
1112  ((MainWindow*) w)->gsubscribe_cb_i();
1113  }
1114 
1115  // Unsubscribe callback
1116  inline void gunsubscribe_cb_i(void)
1117  {
1118  std::size_t index = (std::size_t) groupList->value();
1119  int rv;
1120 
1121  // Ask for confirmation
1122  fl_message_title(S("Warning"));
1123  rv = fl_choice("%s", S("No"),
1124  S("Yes"), NULL,
1125  S("Really unsubscribe group?"));
1126  if(!rv) { return; }
1127 
1128  if(!index)
1129  {
1130  SC("Do not use non-ASCII for the translation of this item")
1131  fl_message_title(S("Error"));
1132  fl_alert("%s", S("No group selected"));
1133  }
1134  else
1135  {
1136  rv = core_unsubscribe_group(&group_num, &group_list,
1137  &group_list_index);
1138  if(rv)
1139  {
1140  fl_message_title(S("Error"));
1141  fl_alert("%s", S("Unsubscribe operation failed"));
1142  }
1143  rv = core_export_group_states(group_num, group_list);
1144  if(rv)
1145  {
1146  fl_message_title(S("Error"));
1147  fl_alert("%s", S("Exporting group states failed"));
1148  }
1149  unsub = true;
1150  groupListRefresh(UI_CB_START);
1151  }
1152  }
1153  static void gunsubscribe_cb(Fl_Widget*, void* w)
1154  {
1155  ((MainWindow*) w)->gunsubscribe_cb_i();
1156  }
1157 
1158  // Group list refresh callback
1159  inline void grefresh_cb_i(void)
1160  {
1161  groupListRefresh(UI_CB_START);
1162  }
1163  static void grefresh_cb(Fl_Widget*, void* w)
1164  {
1165  ((MainWindow*) w)->grefresh_cb_i();
1166  }
1167 
1168  // Sort group list callback
1169  inline void gsort_cb_i(void)
1170  {
1171  int rv;
1172 
1173  // Ask for confirmation
1174  fl_message_title(S("Warning"));
1175  rv = fl_choice("%s", S("No"),
1176  S("Yes"), NULL,
1177  S("Really sort group list?"));
1178  if(!rv) { return; }
1179 
1180  rv = core_sort_group_list();
1181  if(!rv)
1182  {
1183  // Set unsubscribe flag to clear tree and current group/article
1184  unsub = true;
1185  groupListRefresh(UI_CB_START);
1186  }
1187  }
1188  static void gsort_cb(Fl_Widget*, void* w)
1189  {
1190  ((MainWindow*) w)->gsort_cb_i();
1191  }
1192 
1193  // Group select callback
1194  inline void gselect_cb_i(void)
1195  {
1196  groupSelect(UI_CB_START, groupList->value());
1197  }
1198  static void gselect_cb(Fl_Widget*, void* w)
1199  {
1200  ((MainWindow*) w)->gselect_cb_i();
1201  }
1202 
1203  // Next unread group callback
1204  inline void nug_cb_i(void)
1205  {
1206  int i;
1207  int groups = groupList->size();
1208  int current = groupList->value(); // Zero if no group is selected
1209  core_anum_t ur;
1210 
1211  wraparound:
1212  for(current ? i = current - 1 : i = 0; i < groups; ++i)
1213  {
1214  ur = groupGetUnreadNo(subscribedGroups[i].lwm,
1215  subscribedGroups[i].hwm, group_list[i].info);
1216  if(ur)
1217  {
1218  groupList->select(++i);
1219  groupSelect(UI_CB_START, i);
1220  break;
1221  }
1222  }
1223  if (1 < current)
1224  {
1225  groups = current - 1;
1226  current = 0;
1227  goto wraparound;
1228  }
1229  }
1230  static void nug_cb(Fl_Widget*, void* w)
1231  {
1232  ((MainWindow*) w)->nug_cb_i();
1233  }
1234 
1235  // Threaded view toggle callback
1236  // This callback should deactivate all menu items not usable in this mode
1237  inline void thrv_cb_i(void)
1238  {
1239  Fl_Menu_Item* mi = (Fl_Menu_Item*) menu->find_item(uthrv_sort_cb);
1240 
1241  if(config[CONF_TVIEW].val.i)
1242  {
1243  config[CONF_TVIEW].val.i = 0;
1244  if(NULL != mi) { mi->activate(); }
1245  }
1246  else
1247  {
1248  config[CONF_TVIEW].val.i = 1;
1249  if(NULL != mi) { mi->deactivate(); }
1250  }
1251  if(NULL != currentGroup) { updateTree(); }
1252 #if CFG_COCOA_SYS_MENUBAR
1253  menu->update();
1254 #endif // CFG_COCOA_SYS_MENUBAR
1255  }
1256  static void thrv_cb(Fl_Widget*, void* w)
1257  {
1258  ((MainWindow*) w)->thrv_cb_i();
1259  }
1260 
1261  // Unthreaded view sorting based on date or watermark toggle callback
1262  // Ignored for threaded view
1263  inline void uthrv_sort_cb_i(void)
1264  {
1265  if(config[CONF_UTVIEW_AN].val.i) { config[CONF_UTVIEW_AN].val.i = 0; }
1266  else { config[CONF_UTVIEW_AN].val.i = 1; }
1267  if(NULL != currentGroup) { updateTree(); }
1268  }
1269  static void uthrv_sort_cb(Fl_Widget*, void* w)
1270  {
1271  ((MainWindow*) w)->uthrv_sort_cb_i();
1272  }
1273 
1274  // Only unread flag toggle callback
1275  inline void onlyur_cb_i(void)
1276  {
1277  if(config[CONF_ONLYUR].val.i) { config[CONF_ONLYUR].val.i = 0; }
1278  else { config[CONF_ONLYUR].val.i = 1; }
1279  if(NULL != currentGroup) { updateTree(); }
1280  }
1281  static void onlyur_cb(Fl_Widget*, void* w)
1282  {
1283  ((MainWindow*) w)->onlyur_cb_i();
1284  }
1285 
1286  // Wrap text to fit width
1287  inline void wrap_cb_i(void)
1288  {
1289  if(Fl_Text_Display::WRAP_AT_BOUNDS != wrapMode)
1290  {
1291  wrapMode = Fl_Text_Display::WRAP_AT_BOUNDS;
1292  }
1293  else { wrapMode = Fl_Text_Display::WRAP_NONE; }
1294  text->wrap_mode(wrapMode, 0);
1295  }
1296  static void wrap_cb(Fl_Widget*, void* w)
1297  {
1298  ((MainWindow*) w)->wrap_cb_i();
1299  }
1300 
1301  // ROT13 encode/decode callback
1302  inline void rot13_cb_i(void);
1303  static void rot13_cb(Fl_Widget*, void* w)
1304  {
1305  ((MainWindow*) w)->rot13_cb_i();
1306  }
1307 
1308  // Mark single article unread callback
1309  // Now used to toggle (mark read if called again)
1310  inline void msau_cb_i(void);
1311  static void msau_cb(Fl_Widget*, void* w)
1312  {
1313  ((MainWindow*) w)->msau_cb_i();
1314  }
1315 
1316  // Mark subthread read callback
1317  inline void mssar(Fl_Tree_Item*);
1318  inline void mssar_cb_i(void);
1319  static void mssar_cb(Fl_Widget*, void* w)
1320  {
1321  ((MainWindow*) w)->mssar_cb_i();
1322  }
1323 
1324  // Mark all read callback
1325  inline void maar_cb_i(void);
1326  static void maar_cb(Fl_Widget*, void* w)
1327  {
1328  ((MainWindow*) w)->maar_cb_i();
1329  }
1330 
1331  // Mark (all articles in) all groups read callback
1332  inline void magar_cb_i(void);
1333  static void magar_cb(Fl_Widget*, void* w)
1334  {
1335  ((MainWindow*) w)->magar_cb_i();
1336  }
1337 
1338  // Article select callback
1339  inline void aselect_cb_i(void);
1340  static void aselect_cb(Fl_Widget*, void* w)
1341  {
1342  ((MainWindow*) w)->aselect_cb_i();
1343  }
1344 
1345  // View source code callback
1346  inline void viewsrc_cb_i(void)
1347  {
1348  viewSrc(UI_CB_START);
1349  }
1350  static void viewsrc_cb(Fl_Widget*, void* w)
1351  {
1352  ((MainWindow*) w)->viewsrc_cb_i();
1353  }
1354 
1355  // Compose callback
1356  inline void compose_cb_i(void)
1357  {
1358  articleCompose(false, false);
1359  }
1360  static void compose_cb(Fl_Widget*, void* w)
1361  {
1362  ((MainWindow*) w)->compose_cb_i();
1363  }
1364 
1365  // Reply callback
1366  inline void reply_cb_i(void)
1367  {
1368  articleCompose(true, false);
1369  }
1370  static void reply_cb(Fl_Widget*, void* w)
1371  {
1372  ((MainWindow*) w)->reply_cb_i();
1373  }
1374 
1375  // Cancel callback
1376  inline void cancel_cb_i(void)
1377  {
1378  articleCompose(false, true);
1379  }
1380  static void cancel_cb(Fl_Widget*, void* w)
1381  {
1382  ((MainWindow*) w)->cancel_cb_i();
1383  }
1384 
1385  // Supersede callback
1386  inline void supersede_cb_i(void)
1387  {
1388  articleCompose(true, true);
1389  }
1390  static void supersede_cb(Fl_Widget*, void* w)
1391  {
1392  ((MainWindow*) w)->supersede_cb_i();
1393  }
1394 
1395  // Reply by e-mail callback
1396  inline void email_cb_i(void)
1397  {
1398  sendEmail();
1399  }
1400  static void email_cb(Fl_Widget*, void* w)
1401  {
1402  ((MainWindow*) w)->email_cb_i();
1403  }
1404 
1405  // Next unread article callback
1406  inline void nua_cb_i(void)
1407  {
1408  std::size_t index = (std::size_t) groupList->value();
1409 
1410  if(!index)
1411  {
1412  SC("Do not use non-ASCII for the translation of this item")
1413  fl_message_title(S("Error"));
1414  fl_alert("%s", S("No group selected"));
1415  }
1416  else { ascrolldown_cb(false); }
1417  }
1418  static void nua_cb(Fl_Widget*, void* w)
1419  {
1420  ((MainWindow*) w)->nua_cb_i();
1421  }
1422 
1423  // Previous read article callback
1424  inline void pra_cb_i(void)
1425  {
1426  const char* mid;
1427  std::size_t len;
1428 
1429  if(NULL == lastArticleHE)
1430  {
1431  SC("Do not use non-ASCII for the translation of this item")
1432  fl_message_title(S("Error"));
1433  fl_alert("%s", S("No previously read article stored for group"));
1434  }
1435  else
1436  {
1437  mid = lastArticleHE->header->msgid;
1438  len = std::strlen(mid);
1439  // Use Message-ID without angle brackets
1440  if(NULL == searchSelectArticle(&mid[1], len - (std::size_t) 2))
1441  {
1442  SC("Do not use non-ASCII for the translation of this item")
1443  fl_message_title(S("Error"));
1444  fl_alert("%s", "Previous article not found (bug)");
1445  }
1446  }
1447  }
1448  static void pra_cb(Fl_Widget*, void* w)
1449  {
1450  ((MainWindow*) w)->pra_cb_i();
1451  }
1452 
1453  // Hyperlink clicked callback
1454  inline void hyperlink_cb_i(void)
1455  {
1456  hyperlinkHandler(hyperlinkPosition);
1457  }
1458  static void hyperlink_cb(Fl_Widget*, void* w)
1459  {
1460  ((MainWindow*) w)->hyperlink_cb_i();
1461  }
1462 
1463  void clearTree(void);
1464  void scrollTree(ui_scroll, Fl_Tree_Item*);
1465  bool checkTreeBranchForUnread(Fl_Tree_Item*);
1466  bool checkTreeBranchForItem(Fl_Tree_Item*, Fl_Tree_Item*);
1467  Fl_Tree_Item* addTreeNodes(Fl_Tree_Item*, core_hierarchy_element*);
1468  void updateTree(void);
1469  inline void collapseTree(void)
1470  {
1471  int i;
1472 
1473  for(i = 0; i < articleTree->root()->children(); ++i)
1474  {
1475  articleTree->root()->child(i)->close();
1476  }
1477  }
1478  void groupSubscribe(int);
1479  core_anum_t groupGetUnreadNo(core_anum_t, core_anum_t,
1480  struct core_range*);
1481  void groupListUpdateEntry(std::size_t);
1482  int groupStateMerge(void);
1483  void groupSelect(int, int);
1484  void groupCAC(core_groupdesc*);
1485  void groupListGetProposal(int);
1486  void updateArticleTree(int);
1487  void articleSelect(int);
1488  void viewMotd(int);
1489  void viewSrc(int);
1490  void stripAngleAddress(char*);
1491  void articleCompose(bool, bool);
1492  void sendEmail(void);
1493  Fl_Tree_Item* searchSelectArticle(const char*, std::size_t);
1494  void storeMIMEEntityToFile(const char*, const char*);
1495  void hyperlinkHandler(int);
1496  const char* printHeaderFields(struct core_article_header*);
1497  bool stateMachine(enum mainWindowEvent);
1498  public:
1499  void viewArticle(int, const char*);
1500  static void group_list_refresh_timer_cb(void* w)
1501  {
1502  ((MainWindow*) w)->groupListRefresh(UI_CB_START);
1503  if(0 < config[CONF_REFRESH_INTERVAL].val.i)
1504  {
1505  double iv = 60.0 * (double) config[CONF_REFRESH_INTERVAL].val.i;
1506 
1507  // Start timeout for configured refresh interval
1508  Fl::add_timeout(iv, group_list_refresh_timer_cb, (void*) w);
1509  }
1510  }
1511  static void serverconf_cb(void* w)
1512  {
1513  ((MainWindow*) w)->updateServer(UI_CB_CONTINUE);
1514  }
1515  static void subscribe_cb1(void* w)
1516  {
1517  ((MainWindow*) w)->groupSubscribe(UI_CB_CONTINUE);
1518  }
1519  static void subscribe_cb2(void* w)
1520  {
1521  ((MainWindow*) w)->groupSubscribe(UI_CB_FINISH);
1522  }
1523  static void groupproposal_cb(void* w)
1524  {
1525  ((MainWindow*) w)->groupListGetProposal(UI_CB_CONTINUE);
1526  }
1527  static void refresh_cb1(void* w)
1528  {
1529  ((MainWindow*) w)->groupListRefresh(UI_CB_CONTINUE);
1530  }
1531  static void refresh_cb2(void* w)
1532  {
1533  ((MainWindow*) w)->groupListRefresh(UI_CB_FINISH);
1534  }
1535  static void group_cb(void* w)
1536  {
1537  ((MainWindow*) w)->groupSelect(UI_CB_CONTINUE, 0);
1538  }
1539  static void overview_cb(void* w)
1540  {
1541  ((MainWindow*) w)->updateArticleTree(UI_CB_FINISH);
1542  }
1543  static void header_cb(void* w)
1544  {
1545  ((MainWindow*) w)->updateArticleTree(UI_CB_CONTINUE);
1546  }
1547  static void body_cb(void* w)
1548  {
1549  ((MainWindow*) w)->articleSelect(UI_CB_CONTINUE);
1550  }
1551  static void motd_cb(void* w)
1552  {
1553  ((MainWindow*) w)->viewMotd(UI_CB_CONTINUE);
1554  }
1555  static void article_cb(void* w)
1556  {
1557  ((MainWindow*) w)->viewArticle(UI_CB_CONTINUE, NULL);
1558  }
1559  static void src_cb(void* w)
1560  {
1561  ((MainWindow*) w)->viewSrc(UI_CB_CONTINUE);
1562  }
1563  static void post_cb(void* w)
1564  {
1565  ((MainWindow*) w)->articlePost(UI_CB_CONTINUE, NULL);
1566  }
1567  // Search in article content
1568  inline void asearch_cb_i(void);
1569  static void asearch_cb(Fl_Widget*, void* w)
1570  {
1571  ((MainWindow*) w)->asearch_cb_i();
1572  }
1573  void ascrolldown_cb(bool);
1574  void articlePost(int, const char*);
1575  inline void composeComplete(void)
1576  {
1577  if(!stateMachine(EVENT_COMPOSE_EXIT))
1578  {
1579  PRINT_ERROR("Error in main window state machine");
1580  }
1581  }
1582  void calculatePercent(std::size_t, std::size_t);
1583  inline int groupStateExport(void)
1584  {
1585  return(core_export_group_states(group_num, group_list));
1586  }
1587  void updateServer(int);
1588  void groupListRefresh(int);
1589  inline void groupListImport(void)
1590  {
1591  core_destroy_subscribed_group_states(&group_num, &group_list);
1592  groupListRefresh(UI_CB_START);
1593  }
1594  void articleUpdate(Fl_Text_Buffer*);
1595  inline int getTilingX(void) { return(groupList->w()); }
1596  inline int getTilingY(void) { return(articleTree->h()); }
1597  inline void setTilingX(int tx)
1598  {
1599  // Enforce some minimum width
1600  if(50 > tx) { tx = 50; }
1601  if(w() - 50 < tx) { tx = w() - 50; }
1602  contentGroup
1603 #if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1604  // Requires FLTK 1.4.0
1605  ->move_intersection
1606 #else // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1607  ->position
1608 #endif // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1609  (230, 0, tx, 0);
1610  }
1611  inline void setTilingY(int ty)
1612  {
1613  // Enforce some minimum height
1614  if(50 > ty) { ty = 50; }
1615  if(groupList->h() - 50 < ty) { ty = groupList->h() - 50; }
1616  contentGroup2
1617 #if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1618  // Requires FLTK 1.4.0
1619  ->move_intersection
1620 #else // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1621  ->position
1622 #endif // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1623  (1, contentGroup2->y() + 140,
1624  1, contentGroup2->y() + ty);
1625  }
1626  MainWindow(const char*);
1627  ~MainWindow(void);
1628 };
1629 
1630 
1631 // =============================================================================
1632 // Macros
1633 
1634 //! Replace message in main window status bar
1635 #define UI_STATUS(s) { if(mainWindow) { mainWindow->statusBar->label(s); } }
1636 
1637 //! Clear main window progress bar
1638 #define UI_READY() \
1639 { \
1640  if(mainWindow) \
1641  { \
1642  mainWindow->progressBar->value(0.0); \
1643  mainWindow->progressBar->label(""); \
1644  mainWindow->default_cursor(FL_CURSOR_DEFAULT); \
1645  mainWindow->busy = false; \
1646  } \
1647 }
1648 
1649 //! Display "Busy" in main window progress bar
1650 #define UI_BUSY() \
1651 { \
1652  if(mainWindow) \
1653  { \
1654  mainWindow->progressBar->value(0.0); \
1655  SC("This string must fit into the progress bar box") \
1656  mainWindow->progressBar->label(S("Busy")); \
1657  mainWindow->default_cursor(FL_CURSOR_WAIT); \
1658  mainWindow->busy = true; \
1659  } \
1660 }
1661 
1662 //! Update value of main window progress bar
1663 #define UI_PROGRESS(s, e) \
1664 { \
1665  if(mainWindow) \
1666  { \
1667  mainWindow->calculatePercent(s, e); \
1668  if(100.0 == progress_percent_value || !progress_skip_update) \
1669  { \
1670  mainWindow->progressBar->value(progress_percent_value); \
1671  mainWindow->progressBar->copy_label(progress_percent_label); \
1672  } \
1673  if(!mainWindow->busy) \
1674  { \
1675  mainWindow->default_cursor(FL_CURSOR_WAIT); \
1676  mainWindow->busy = true; \
1677  } \
1678  Fl::check(); \
1679  progress_skip_update = true; \
1680  Fl::add_timeout(0.1, progress_release_cb, this); \
1681  } \
1682 }
1683 
1684 
1685 // =============================================================================
1686 // Variables
1687 
1688 // Pixmaps
1689 extern "C"
1690 {
1691 #include "pixmaps/pixmaps.c"
1692 }
1693 #if USE_WINDOW_ICON
1694 static Fl_Pixmap pm_window_icon(xpm_window_icon); // Icon for main window
1695 #endif // USE_WINDOW_ICON
1696 static Fl_Pixmap pm_own(xpm_own); // Icon for own articles
1697 static Fl_Pixmap pm_reply_to_own(xpm_reply_to_own); // Icon for own articles
1698 static Fl_Pixmap pm_score_down(xpm_score_down); // Icon for negative score
1699 static Fl_Pixmap pm_score_up(xpm_score_up); // Icon for positive score
1700 
1701 static Fl_Text_Buffer* dummyTb;
1702 #if USE_WINDOW_ICON
1703 static Fl_RGB_Image mainIcon(&pm_window_icon);
1704 #endif // USE_WINDOW_ICON
1705 static MainWindow* mainWindow = NULL;
1706 static ProtocolConsole* protocolConsole = NULL;
1707 static int exitRequest = 0;
1708 static bool lockingInitialized = false;
1709 static int offset_correction_x;
1710 static int offset_correction_y;
1711 
1712 
1713 // =============================================================================
1714 // Set default font
1715 // This is for internal size calculations, changing this will not display the
1716 // widgets with a different font!
1717 
1718 static void gui_set_default_font(void)
1719 {
1720  fl_font(FL_HELVETICA, FL_NORMAL_SIZE);
1721 }
1722 
1723 
1724 // =============================================================================
1725 // Greeting message (displayed as the default article on startup)
1726 
1727 static const char* gui_greeting(void)
1728 {
1729  std::ostringstream greetingString;
1730  const char* s1;
1731  char* s2;
1732  std::size_t len;
1733 
1734  // Create greeting string
1735  if(!std::strcmp(CFG_NAME, "flnews"))
1736  {
1737  greetingString
1738  << " ______ ___\n"
1739  << " / ___/ / /\n"
1740  << " / / / /\n"
1741  << " __/ /_ / / _______ _______ ___ ___ _______\n"
1742  << " /_ __/ / / / ___ \\ / ___ \\ / / / / / _____\\\n"
1743  << " / / / / / / / / / /__/__/ / /_/|_/ / /__/_____\n"
1744  << " / / / / / / / / / /_____ / ___ / ______/ /\n"
1745  << "/__/ /__/ /__/ /__/ \\_______/ \\__/ /___/ \\_______/";
1746  }
1747  else { greetingString << CFG_NAME; }
1748  greetingString << "\n\n"
1749  << S("A fast and lightweight Usenet newsreader for Unix.") << "\n\n"
1750  << CFG_NAME << " "
1751  // Display credits to the authors of libraries we use
1752  << S("is based in part on the work of the following projects:") << "\n"
1753  << "- FLTK project (http://www.fltk.org/)" << "\n"
1754  << "- The Unicode\xC2\xAE Standard (http://www.unicode.org/)" << "\n"
1755  << " Unicode is a registered trademark of Unicode, Inc. in the" << "\n"
1756  << " United States and other countries" << "\n"
1757 #if CFG_USE_TLS
1758 # if CFG_USE_LIBRESSL
1759  << "- LibreSSL project (http://www.libressl.org/)" << "\n"
1760 # else // CFG_USE_LIBRESSL
1761  << "- OpenSSL project (https://www.openssl.org/)" << "\n"
1762 # endif // CFG_USE_LIBRESSL
1763  << " This product includes software developed by" << "\n"
1764  << " the OpenSSL Project for use in the OpenSSL Toolkit" << "\n"
1765  << " This product includes software written by" << "\n"
1766  << " Tim Hudson <tjh@cryptsoft.com>" << "\n"
1767  << " This product includes cryptographic software written by" << "\n"
1768  << " Eric Young <eay@cryptsoft.com>" << "\n"
1769 #endif // CFG_USE_TLS
1770 #if CFG_USE_ZLIB
1771  << "- zlib compression library (http://zlib.net/)" << "\n"
1772 #endif // CFG_USE_ZLIB
1773  << std::flush;
1774 
1775  // Assigning a name to the temporary string object forces it to stay in
1776  // memory as long as its name go out of scope (this is what we need).
1777  const std::string& gs = greetingString.str();
1778 
1779  // Allocate memory and copy greeting string
1780  s1 = gs.c_str();
1781  len = std::strlen(s1) + (std::size_t) 1;
1782  s2 = new char[len];
1783  std::memcpy(s2, s1, len);
1784 
1785  return(s2);
1786 }
1787 
1788 
1789 // =============================================================================
1790 // UTF-8 to ISO 8859-1 conversion for window manager
1791 // The caller is responsible to release the memory allocated for the result
1792 
1793 #if CFG_USE_XSI && !CFG_NLS_DISABLE
1794 static const char* gui_utf8_iso(const char* s)
1795 {
1796  const char* res = NULL;
1797  unsigned int len;
1798  char* tmp;
1799 
1800  // Check whether locale use UTF-8 (Decision based on 'LC_CTYPE')
1801  if(!fl_utf8locale())
1802  {
1803  // No => Expect that the WM can display ISO 8859-1
1804  //PRINT_ERROR("Non UTF-8 locale, converting string to ISO 8859-1");
1805  len = fl_utf8toa(s, (unsigned int) std::strlen(s), NULL, 0);
1806  tmp = new char[len + 1U];
1807  if(fl_utf8toa(s, (unsigned int) std::strlen(s), tmp, len + 1U) != len)
1808  {
1809  delete[] tmp;
1810  }
1811  else { res = (const char*) tmp; }
1812  }
1813  else
1814  {
1815  // Yes => Return string unchanged
1816  len = (unsigned int) std::strlen(s);
1817  tmp = new char[len + 1U];
1818  std::strncpy(tmp, s, len + 1U);
1819  res = (const char*) tmp;
1820  }
1821  // Return NULL if conversion has failed or is not supported
1822  return(res);
1823 }
1824 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
1825 
1826 
1827 // =============================================================================
1828 // Store content of FLTK text buffer to file
1829 // The encoding of the pathname is converted to the encoding of the locale
1830 // Returns zero on success
1831 
1832 
1833 static int gui_save_to_file(const char* pathname, Fl_Text_Buffer* tbuf)
1834 {
1835  int rv = -1;
1836  char* s = tbuf->text();
1837 
1838  if(NULL != s)
1839  {
1840  rv = core_save_to_file(pathname, s);
1841  std::free((void*) s);
1842  }
1843 
1844  return(rv);
1845 }
1846 
1847 
1848 // =============================================================================
1849 // Check whether article is suitable for display.
1850 //
1851 // FLTK 1.3 has problems with very long lines. The lines are not rendered
1852 // correctly (so that the horizontal scrollbar can be used). There are reports
1853 // that displaying such text have crashed the whole application.
1854 // This bug is fixed in FLTK 1.4.
1855 //
1856 // This function should check for problematic content and replace the data in
1857 // the text buffer tbuf with an appropriate error message.
1858 
1859 static void gui_check_article(Fl_Text_Buffer* tbuf)
1860 {
1861  int error = 0;
1862 
1863 #if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1864  // NOP
1865 #else // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1866  // Search for lines longer than 1000 octets (including CRLF).
1867  // An article conforming to RFC 5536 is not allowed to contain such lines.
1868  int len = tbuf->length();
1869  int pos = 0;
1870  int eol;
1871 
1872  while (pos < len)
1873  {
1874  if (0 < pos)
1875  {
1876  ++pos; // Skip newline
1877  }
1878  eol = tbuf->line_end(pos);
1879  if (1000 < eol - pos)
1880  {
1881  // Line length error detected
1882  error = 1;
1883  break;
1884  }
1885  pos = eol;
1886  }
1887 #endif // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
1888 
1889  if (error)
1890  {
1891  // The delimiter is required for font selection and citation
1892  tbuf->text(ENC_DELIMITER);
1893  tbuf->append("[Error]\n");
1894  tbuf->append(S("Invalid line length"));
1895  }
1896 }
1897 
1898 
1899 // =============================================================================
1900 // Some FLTK backends always display SHY, others never display SHY.
1901 // The first variant is correct in the sense of ISO 8859-1, but is not
1902 // the intended behaviour of most users. The second variant is wrong,
1903 // because Unicode specifies that a SHY at the end of a line (where a
1904 // linebreak was inserted by rewrap algorithm) should be visible.
1905 // This should give consistent presentation of Soft Hyphen (SHY):
1906 // - Delete all SHY characters not at the end of a line
1907 // - Replace SHY at the end of a line with Hyphen-Minus
1908 
1909 static void gui_process_shy(Fl_Text_Buffer* tbuf)
1910 {
1911  int pos_shy = 0; // Search for soft hyphen from this position
1912  int rv, rv2;
1913 
1914  do
1915  {
1916  rv = tbuf->findchar_forward(pos_shy, 0x00ADU, &pos_shy);
1917  if(rv)
1918  {
1919  // Check next character
1920  rv2 = tbuf->next_char(pos_shy);
1921  if(0x000AU == tbuf->char_at(rv2))
1922  {
1923  // LF => Replace SHY with hyphen-minus (at end of line)
1924  tbuf->replace(pos_shy, rv2, "-");
1925  }
1926  else { tbuf->remove(pos_shy, rv2); }
1927  }
1928  }
1929  while(rv);
1930 }
1931 
1932 
1933 // =============================================================================
1934 // Check subject line for "Re: " and report position behind (for replies)
1935 // The caller must insert a "Re: " before that position to create a reply.
1936 
1937 static const char* gui_check_re_prefix(const char* subject)
1938 {
1939  bool prefix_replaced; // Nonstandard subject prefix replaced with "Re: "
1940 
1941  if((std::size_t) 3 <= std::strlen(subject))
1942  {
1943  // To be more tolerant, also accept "Re:", "RE:", "RE: ", "re:", "re: ",
1944  // "AW:", "AW: ", "Aw:" and "Aw: "
1945  do
1946  {
1947  prefix_replaced = false;
1948  if( (!std::strncmp(subject, "Re:", 3) && ' ' != subject[3])
1949  || !std::strncmp(subject, "RE:", 3)
1950  || !std::strncmp(subject, "re:", 3)
1951  || !std::strncmp(subject, "AW:", 3)
1952  || !std::strncmp(subject, "Aw:", 3) )
1953  {
1954  PRINT_ERROR("Nonstandard subject prefix"
1955  " replaced with \"Re: \"");
1956  subject = &subject[3];
1957  if(' ' == subject[0]) { subject = &subject[1]; }
1958  prefix_replaced = true;
1959  }
1960  }
1961  while(prefix_replaced);
1962  // Standard variant
1963  if(!std::strncmp(subject, "Re: ", 4)) { subject = &subject[4]; }
1964  }
1965 
1966  return(subject);
1967 }
1968 
1969 
1970 // =============================================================================
1971 // Create comma separated list of newsgroups
1972 // The names of the newsgroups must be passed as NULL-terminated 'array'.
1973 // The caller is responsible to delete[] the memory for the result string
1974 
1975 static const char* gui_create_newsgroup_list(const char** array)
1976 {
1977  static const char error[] = "[Error]";
1978  char* res = NULL;
1979  std::size_t size_max = (std::size_t) -1;
1980  std::size_t i = 0;
1981  std::size_t len = 0;
1982  std::size_t j;
1983 
1984  // Calculate required memory size
1985  while(NULL != array[i])
1986  {
1987  if(UINT_MAX == i) { break; }
1988  j = std::strlen(array[i]);
1989  if(size_max - len < j) { break; }
1990  len += j;
1991  ++i;
1992  }
1993  if(!i || (size_max - len < i))
1994  {
1995  res = new char[std::strlen(error) + (size_t) 1];
1996  std::strcat(res, error);
1997  }
1998  else
1999  {
2000  // Additional memory for comma separators and NUL termination
2001  len += i;
2002  // Create newsgroup list
2003  res = new char[len];
2004  res[0] = 0;
2005  for(j = 0; i > j; ++j)
2006  {
2007  if(j) { std::strcat(res, ","); }
2008  std::strcat(res, array[j]);
2009  }
2010  res[len - (std::size_t) 1] = 0;
2011  }
2012 
2013  return(res);
2014 }
2015 
2016 
2017 // =============================================================================
2018 // Check for signature separator to be the last one in MIME entity
2019 
2020 static int gui_last_sig_separator(char* buf)
2021 {
2022  int res = 1;
2023  char* p;
2024  char* q;
2025 
2026  // Attention: Overloaded and both prototypes different than in C!
2027  p = std::strstr(buf, "\n-- \n");
2028  if(NULL != p)
2029  {
2030  res = 0;
2031  // Check for MIME entity separator before next signature separator
2032  // Attention: Overloaded and both prototypes different than in C!
2033  q = std::strstr(buf, "\n________________________________________"
2034  "_______________________________________|\n");
2035  if(NULL != q && p > q) { res = 1; }
2036  }
2037 
2038  return(res);
2039 }
2040 
2041 
2042 // =============================================================================
2043 // Add introduction line and cite content (for replies)
2044 // The name of the cited author must be passed as 'ca'.
2045 // The array with newsgroup names of cited article must be passed as 'nga'.
2046 
2047 static void gui_cite_content(Fl_Text_Buffer* compText, const char* ca,
2048  const char** nga)
2049 {
2050  const char* ngl = gui_create_newsgroup_list(nga);
2051  const char* qm; // Quote mark
2052  int rv = 1;
2053  int pos = 0;
2054  int pos_found;
2055  bool fl = true; // Flag indicating first level citation
2056  unsigned int c;
2057  unsigned int next;
2058  const char* intro;
2059 
2060  // Set quote mark style according to config file
2061  switch(config[CONF_QUOTESTYLE].val.i)
2062  {
2063  case 0: { qm = ">"; break; }
2064  case 1: { qm = "> "; break; }
2065  default:
2066  {
2067  PRINT_ERROR("Quoting style configuration not supported");
2068  // Use default from old versions that can't be configured
2069  qm = "> ";
2070  break;
2071  }
2072  }
2073  // Quote original content
2074  if(compText->length())
2075  {
2076  while(compText->char_at(pos))
2077  {
2078  // Check for second level citation marks
2079  if((unsigned int) '>' == compText->char_at(pos))
2080  {
2081  fl = false;
2082  }
2083  // Add additional citation mark
2084  compText->insert(pos, qm);
2085  // Unify spacing between and after citation marks if configured
2086  if(config[CONF_QUOTEUNIFY].val.i)
2087  {
2088  if(!config[CONF_QUOTESTYLE].val.i)
2089  {
2090  do
2091  {
2092  c = compText->char_at(pos);
2093  next = compText->char_at(compText->next_char(pos));
2094  if((unsigned int) '>' == c)
2095  {
2096  if((unsigned int) ' ' == next)
2097  {
2098  // Remove space between marks: "> " => ">"
2099  compText->remove(pos + 1, pos + 2);
2100  }
2101  }
2102  pos = compText->next_char(pos);
2103  next = compText->char_at(pos);
2104  }
2105  while((unsigned int) '>' == next || (unsigned int) ' ' == next);
2106  }
2107  else
2108  {
2109  do
2110  {
2111  c = compText->char_at(pos);
2112  next = compText->char_at(compText->next_char(pos));
2113  if((unsigned int) '>' == c)
2114  {
2115  if((unsigned int) '>' == next)
2116  {
2117  // Add missing space between marks: ">>" => "> >"
2118  compText->insert(++pos, " ");
2119  }
2120  else if((unsigned int) '>' != next
2121  && (unsigned int) ' ' != next)
2122  {
2123  // Missing space after last mark "> >" => "> > "
2124  compText->insert(++pos, " ");
2125  }
2126  }
2127  pos = compText->next_char(pos);
2128  }
2129  while((unsigned int) '>' == next || (unsigned int) ' ' == next);
2130  }
2131  }
2132  // Search next EOL
2133  rv = compText->findchar_forward(pos, (unsigned int) '\n',
2134  &pos_found);
2135  if(!rv) { break; } else { pos = ++pos_found; }
2136  }
2137  }
2138  // Prepend new introduction line
2139  if(fl)
2140  {
2141  // Insert empty line between intro lines and cited content
2142  compText->insert(0, "\n"); compText->insert(0, qm);
2143  }
2144  // Prepend new introduction line with cited authors name
2145  intro = core_get_introduction(ca, ngl);
2146  if(NULL == intro) { compText->insert(0, "[Error] wrote:\n"); }
2147  else
2148  {
2149  compText->insert(0, "\n");
2150  compText->insert(0, intro);
2151  }
2152  core_free((void*) intro);
2153  delete[] ngl;
2154  // Insert empty line after citation
2155  compText->append("\n");
2156 }
2157 
2158 
2159 // =============================================================================
2160 // Print some header fields of article
2161 //
2162 // The caller is responsible to free the memory allocated for the returned
2163 // string (if the result is not NULL).
2164 
2165 #define UI_HDR_FIELDS (std::size_t) 14
2166 #define UI_HDR_PAD(s, n) { for(i = 0; i < n; ++i) { s << " "; } }
2167 static const char* gui_print_header_fields(struct core_article_header* h)
2168 {
2169  static const char* f[UI_HDR_FIELDS] =
2170  {
2171  S("Subject"),
2172  S("Date"),
2173  S("From"),
2174  S("Reply-To"),
2175  S("Newsgroups"),
2176  S("Followup-To"),
2177  S("Distribution"),
2178  S("Message-ID"),
2179  S("Supersedes"),
2180  S("Organization"),
2181  S("User-Agent"),
2182  S("Transfer-Encoding"),
2183  S("Content-Type"),
2184  S("References")
2185  };
2186  std::size_t flen[UI_HDR_FIELDS];
2187  std::size_t fpad[UI_HDR_FIELDS];
2188  std::ostringstream hdrData;
2189  char* s = NULL;
2190  std::size_t len;
2191  char date[20];
2192  int rv;
2193  std::size_t i;
2194  std::size_t largest = 0;
2195 
2196  // Calculate length of largest header field ID and padding for the others
2197  for(i = 0; i < UI_HDR_FIELDS; ++i)
2198  {
2199  len = std::strlen(f[i]);
2200 #if CFG_USE_XSI && !CFG_NLS_DISABLE
2201  // Attention: The header field names can be translated if NLS is enabled.
2202  // The resulting Unicode strings may have a different number of characters
2203  // than bytes!
2204  if(INT_MAX < len) { len = INT_MAX; }
2205  rv = fl_utf_nb_char((const unsigned char*) f[i], (int) len);
2206  if(0 > rv) { flen[i] = len; }
2207  else { flen[i] = (std::size_t) rv; }
2208 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
2209  // Not translated header field names are US-ASCII by definition
2210  flen[i] = len;
2211 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
2212  }
2213  for(i = 0; i < UI_HDR_FIELDS; ++i)
2214  {
2215  if(largest < flen[i]) { largest = flen[i]; }
2216  }
2217  for(i = 0; i < UI_HDR_FIELDS; ++i) { fpad[i] = largest - flen[i]; }
2218 
2219  // Copy header field content data
2220  UI_HDR_PAD(hdrData, fpad[0]);
2221  hdrData << f[0] << ": " << h->subject << "\n";
2222  rv = enc_convert_posix_to_iso8601(date, h->date);
2223  if(!rv)
2224  {
2225  UI_HDR_PAD(hdrData, fpad[1]);
2226  hdrData << f[1] << ": " << date << "\n";
2227  }
2228  UI_HDR_PAD(hdrData, fpad[2]);
2229  hdrData << f[2] << ": " << h->from << "\n";
2230  if(NULL != h->reply2)
2231  {
2232  if(std::strcmp(h->from, h->reply2))
2233  {
2234  UI_HDR_PAD(hdrData, fpad[3]);
2235  hdrData << f[3] << ": " << h->reply2 << "\n";
2236  }
2237  }
2238  UI_HDR_PAD(hdrData, fpad[4]);
2239  hdrData << f[4] << ": ";
2240  i = 0;
2241  while(NULL != h->groups[i])
2242  {
2243  if(i) { hdrData << ","; }
2244  hdrData << h->groups[i++];
2245  }
2246  hdrData << "\n";
2247  if(NULL != h->fup2)
2248  {
2249  UI_HDR_PAD(hdrData, fpad[5]);
2250  hdrData << f[5] << ": " << h->fup2 << "\n";
2251  }
2252 
2253  if(NULL != h->dist)
2254  {
2255  UI_HDR_PAD(hdrData, fpad[6]);
2256  hdrData << f[6] << ": " << h->dist << "\n";
2257  }
2258 
2259  UI_HDR_PAD(hdrData, fpad[7]);
2260  hdrData << f[7] << ": " << h->msgid << "\n";
2261  if(NULL != h->supers)
2262  {
2263  UI_HDR_PAD(hdrData, fpad[8]);
2264  hdrData << f[8] << ": " << h->supers << "\n";
2265  }
2266  if(NULL != h->org)
2267  {
2268  UI_HDR_PAD(hdrData, fpad[9]);
2269  hdrData << f[9] << ": " << h->org << "\n";
2270  }
2271  if(NULL != h->uagent)
2272  {
2273  UI_HDR_PAD(hdrData, fpad[10]);
2274  hdrData << f[10] << ": " << h->uagent << "\n";
2275  }
2276  else if(NULL != h->x_newsr)
2277  {
2278  UI_HDR_PAD(hdrData, fpad[10]);
2279  hdrData << f[10] << ": " << h->x_newsr << "\n";
2280  }
2281  else if(NULL != h->x_pagent)
2282  {
2283  UI_HDR_PAD(hdrData, fpad[10]);
2284  hdrData << f[10] << ": " << h->x_pagent << "\n";
2285  }
2286  else if(NULL != h->x_mailer)
2287  {
2288  UI_HDR_PAD(hdrData, fpad[10]);
2289  hdrData << f[10] << ": " << h->x_mailer << "\n";
2290  }
2291  if(NULL != h->mime_cte)
2292  {
2293  UI_HDR_PAD(hdrData, fpad[11]);
2294  hdrData << f[11] << ": " << h->mime_cte << "\n";
2295  }
2296  if(NULL != h->mime_ct)
2297  {
2298  UI_HDR_PAD(hdrData, fpad[12]);
2299  hdrData << f[12] << ": " << h->mime_ct << "\n";
2300  }
2301  if(NULL != h->refs)
2302  {
2303  UI_HDR_PAD(hdrData, fpad[13]);
2304  // The GS control character marks the beginning of the reference list
2305  // The syntax highlighting code should replace it with SP later
2306  hdrData << f[13] << ":" << "\x1D";
2307  i = 0;
2308  while(NULL != h->refs[i]) { hdrData << "[" << i++ << "]"; }
2309  hdrData << "\n";
2310  }
2311  hdrData << std::flush;
2312 
2313  // Attention:
2314  //
2315  // const char* hs = hdrData.str().c_str()
2316  //
2317  // creates 'hs' with undefined value because the compiler is allowed to
2318  // free the temporary string object returned by 'str()' immediately after
2319  // the assignment!
2320  // Assigning names to temporary string objects forces them to stay in
2321  // memory as long as their names go out of scope (this is what we need).
2322  const std::string& hs = hdrData.str();
2323 
2324  len = std::strlen(hs.c_str());
2325  s = new char[++len];
2326  std::strncpy(s, hs.c_str(), len);
2327 
2328  return(s);
2329 }
2330 
2331 
2332 // =============================================================================
2333 // Create RFC 8089 conformant link to save entity to a file
2334 //
2335 // We use a reserved subdirectory in CFG_NAME to avoid clashes with links
2336 // created by the author of the article.
2337 //
2338 // The parameter msgid must point to a string with a valid Message-ID.
2339 //
2340 // The caller is responsible to free() the memory allocated for the link if
2341 // the result is not NULL.
2342 
2343 static const char* gui_create_link_to_entity(const char* msgid,
2344  std::size_t entity)
2345 {
2346  const char* link = NULL;
2347  Fl_Text_Buffer tb;
2348  const char* confdir = NULL;
2349  char* msgid_buf;
2350  std::size_t len;
2351  std::ostringstream entity_no;
2352 
2353  confdir = xdg_get_confdir(CFG_NAME);
2354  if(NULL != confdir)
2355  {
2356  tb.text("<file://");
2357  tb.append(confdir);
2358  tb.append("/.mime/entities/");
2359  // Append Message-ID without angle brackets
2360  len = std::strlen(msgid);
2361  msgid_buf = new char[len];
2362  std::strncpy(msgid_buf, &msgid[1], --len);
2363  msgid_buf[--len] = 0;
2364  tb.append(msgid_buf);
2365  delete[] msgid_buf;
2366  tb.append("/");
2367  // Append entity index
2368  // Note: We cannot use 'posix_snprintf()' here
2369  entity_no.str(std::string());
2370  entity_no << entity << std::flush;
2371  const std::string& s = entity_no.str();
2372  tb.append(s.c_str());
2373  // Append closing delimiter
2374  tb.append(">");
2375  // Copy complete link
2376  link = tb.text();
2377  }
2378  core_free((void*) confdir);
2379 
2380  return(link);
2381 }
2382 
2383 
2384 // =============================================================================
2385 // Decode MIME entities into FLTK text buffer
2386 //
2387 // The parameter tb must be a valid pointer to a FLTK text buffer.
2388 // The parameter mimeData is allowed to be NULL.
2389 
2390 static void gui_decode_mime_entities(Fl_Text_Buffer *tb,
2391  MIMEContent* mimeData,
2392  const char* msgid)
2393 {
2394  const char* p = NULL;
2395  const char* q = NULL;
2396  std::size_t i;
2397  std::size_t first;
2398  std::size_t num;
2399  enc_mime_cte cte = ENC_CTE_UNKNOWN;
2400  enc_mime_ct* ct = NULL;
2401  const char* header;
2402  const char* body;
2403  char last_char;
2404  int attachment;
2405 
2406  if(NULL == mimeData)
2407  {
2408  tb->append(S("MIME content handling error"));
2409  }
2410  else
2411  {
2412  first = 0;
2413  num = mimeData->parts();
2414  // Append MIME entities to text buffer
2415  for(i = first; i < num; ++i)
2416  {
2417  body = mimeData->part(i, &cte, &ct);
2418  // Print delimiter between entities
2419  if(i)
2420  {
2421  // Append a linefeed if last entity doesn't has one at its end
2422  last_char = tb->byte_at(tb->length() - 1);
2423  if((char) 0x0A != last_char) { tb->append("\n"); }
2424  tb->append(ENC_DELIMITER);
2425  }
2426  // Print own headers for multipart entities
2427  if(mimeData->is_multipart())
2428  {
2429  header = mimeData->part_header(i);
2430  if(NULL != header)
2431  {
2432  tb->append(header);
2433  tb->append("\n");
2434  }
2435  }
2436  // Check content disposition
2437  attachment = 0;
2438  if(ENC_CD_ATTACHMENT == mimeData->type(i)) { attachment = 1; }
2439  if(attachment)
2440  {
2441  // Respect content disposition declaration "attachment"
2442  tb->append(S("Declared as attachment by sender"));
2443  // Display RFC 8089 conformant link to save entity to a file.
2444  tb->append("\n");
2445  p = gui_create_link_to_entity(msgid, i);
2446  if(NULL == p) { tb->append("[Error]"); }
2447  else { tb->append(p); }
2448  std::free((void*) p);
2449  }
2450  // RFC 2049 requires that unknown subtypes of "text" must be
2451  // viewable raw/undecoded => We handle them as subtype "plain"
2452  // (print them undecoded)
2453  else if(ENC_CT_TEXT != ct->type && ENC_CT_MESSAGE != ct->type)
2454  {
2455  // Error: Content is not supported
2456  tb->append(S("MIME content type not supported"));
2457  // Display RFC 8089 conformant link to save entity to a file.
2458  tb->append("\n");
2459  p = gui_create_link_to_entity(msgid, i);
2460  if(NULL == p) { tb->append("[Error]"); }
2461  else { tb->append(p); }
2462  std::free((void*) p);
2463  }
2464  else
2465  {
2466  // Decode 'text/plain' content and convert it to Unicode
2467  p = enc_mime_decode(cte, ct->charset, body);
2468  if(NULL == p)
2469  {
2470  tb->append(S("MIME content decoding failed"));
2471  }
2472  else
2473  {
2474  // Handle content with "Format=Flowed" attribute
2475  if(ct->flags & ENC_CT_FLAG_FLOWED)
2476  {
2477  q = enc_mime_flowed_decode(p,
2478  ct->flags & ENC_CT_FLAG_DELSP,
2479  ct->flags & ENC_CT_FLAG_INSLINE);
2480  if(NULL == q)
2481  {
2482  PRINT_ERROR("Decoding of MIME Format=Flowed"
2483  " content failed");
2484  }
2485  else
2486  {
2487  if(p != q && p != body) { enc_free((void*) p); }
2488  p = q;
2489  }
2490  }
2491  // Convert article content line breaks from canonical
2492  // (RFC 822) form to POSIX form
2493  q = core_convert_canonical_to_posix(p, 1, 0);
2494  if(NULL == q)
2495  {
2496  tb->append(
2497  S("Conversion from canonical to local form failed"));
2498  }
2499  else
2500  {
2501  // Free memory allocated by MIME decoder (if any)
2502  if(p != q && p != body) { enc_free((void*) p); }
2503  p = q;
2504  // Success
2505  tb->append(p);
2506  if(p != body) { enc_free((void*) p); }
2507  }
2508  }
2509  }
2510  }
2511  }
2512 }
2513 
2514 
2515 // =============================================================================
2516 // Check whether file exists and ask user if OK to overwrite
2517 
2518 static int gui_check_pathname(const char* pathname)
2519 {
2520  int res = 0;
2521  int rv;
2522 
2523  rv = core_check_file_exist(pathname);
2524  if (0 == rv)
2525  {
2526  // File exists
2527  fl_message_title(S("Warning"));
2528  rv = !fl_choice("%s", S("Cancel"),
2529  S("OK"), NULL,
2530  S("File exists\nReally continue?"));
2531  if(rv)
2532  {
2533  // User does not want to overwrite the file
2534  res = -1;
2535  }
2536  }
2537 
2538  return(res);
2539 }
2540 
2541 
2542 // =============================================================================
2543 // Populate group list
2544 
2545 static int gui_populate_group_list(const char* grouplist)
2546 {
2547  int res = 0;
2548  std::size_t len;
2549  char* buf;
2550  int rv;
2551  int pos = 0;
2552  int pos_new;
2553 
2554  if(NULL != grouplist)
2555  {
2556  len = std::strlen(grouplist);
2557  if(len)
2558  {
2559  buf = new char[len + (std::size_t) 1];
2560  while(1)
2561  {
2562  rv = std::sscanf(&grouplist[pos], "%s%n", buf, &pos_new);
2563  if (1 != rv || !pos_new) { break; }
2564  else
2565  {
2566  pos += pos_new;
2567  (void) core_subscribe_group(buf);
2568  // Ignore potential error and continue with next group
2569  }
2570  }
2571  delete[] buf;
2572  }
2573  }
2574 
2575  return(res);
2576 }
2577 
2578 
2579 // =============================================================================
2580 // My_Text_Display event handler
2581 
2582 int My_Text_Display::handle(int event)
2583 {
2584  int res = 0;
2585  int button;
2586  int x, y;
2587  int pos;
2588  unsigned int style;
2589 
2590  switch(event)
2591  {
2592  case FL_KEYDOWN:
2593  {
2594  // Space key pressed
2595  if(Fl::event_key() == 0x20)
2596  {
2597  // Scroll down
2598  mainWindow->ascrolldown_cb(true);
2599  res = 1;
2600  }
2601  // Slash key pressed
2602  else if(!std::strcmp(Fl::event_text(), "/"))
2603  {
2604  mainWindow->asearch_cb_i();
2605  res = 1;
2606  }
2607  break;
2608  }
2609  case FL_MOVE:
2610  {
2611  // Mouse has moved inside the widget
2612  if(NULL != mStyleBuffer)
2613  {
2614  x = Fl::event_x();
2615  y = Fl::event_y();
2616  pos = xy_to_position(x, y);
2617  style = mStyleBuffer->char_at(pos);
2618  if(NULL != mainWindow)
2619  {
2620  if(style == mainWindow->hyperlinkStyle)
2621  {
2622  // Mouse over hyperlink => Change mouse pointer to 'hand'
2623  fl_cursor(FL_CURSOR_HAND);
2624  }
2625  else { fl_cursor(FL_CURSOR_DEFAULT); }
2626  res = 1;
2627  }
2628  }
2629  break;
2630  }
2631  case FL_PUSH:
2632  case FL_RELEASE:
2633  {
2634  // Mouse button pressed or released
2635  if(NULL != mStyleBuffer)
2636  {
2637  button = Fl::event_button();
2638  if(FL_LEFT_MOUSE == button)
2639  {
2640  // Left mouse button detected
2641  x = Fl::event_x();
2642  y = Fl::event_y();
2643  pos = xy_to_position(x, y);
2644  style = mStyleBuffer->char_at(pos);
2645  // Check whether click hit hyperlink
2646  if(NULL != mainWindow)
2647  {
2648  if(style == mainWindow->hyperlinkStyle)
2649  {
2650  if(FL_PUSH == event) { linkPushed = pos; }
2651  else if(pos == linkPushed)
2652  {
2653  // Click on hyperlink detected => Execute callback
2654  // std::printf("Click on hyperlink detected\n");
2655  linkPushed = -1;
2656  mainWindow->hyperlinkPosition = pos;
2657  res = 1;
2658  do_callback();
2659  break;
2660  }
2661  }
2662  }
2663  }
2664  if(FL_RELEASE == event) { linkPushed = -1; }
2665  }
2666  break;
2667  }
2668  default:
2669  {
2670  break;
2671  }
2672  }
2673  // Other events go to the default handler
2674  if(!res) { res = Fl_Text_Display::handle(event); }
2675 
2676  return(res);
2677 }
2678 
2679 
2680 // =============================================================================
2681 // My_Tree event handler
2682 //
2683 // Since 1.3.1 the old Fl_Tree widget provides a 'get_item_focus()' method.
2684 // Since 1.3.3 the new Fl_Tree widget provides a 'next_visible_item()' method.
2685 
2686 int My_Tree::handle(int event)
2687 {
2688  int res = 0;
2689 
2690  // Consume and ignore events while update of widget is in progress
2691  if(update_in_progress())
2692  {
2693  res = 1;
2694  }
2695  // Intercept key pressed events
2696  else switch(event)
2697  {
2698  case FL_KEYDOWN:
2699  {
2700  // Down (cursor) key
2701 #ifdef FL_ABI_VERSION
2702 # if 10303 <= FL_ABI_VERSION
2703  // Requires FLTK 1.3.3
2704  if(Fl::event_key() == FL_Down)
2705  {
2706  Fl_Tree_Item* ti = get_item_focus();
2707  if(NULL != ti && NULL == next_visible_item(ti, FL_Down))
2708  {
2709  res = 1;
2710  break;
2711  }
2712  }
2713  else
2714 # endif // 10303 <= FL_ABI_VERSION
2715 #endif // FL_ABI_VERSION
2716  // Enter key
2717  if(Fl::event_key() == FL_Enter)
2718  {
2719 #ifdef FL_ABI_VERSION
2720 # if 10301 <= FL_ABI_VERSION
2721  // Requires FLTK 1.3.1
2722  // Looks like there is no safe workaround to emulate with 1.3.0
2723  Fl_Tree_Item* ti = first_selected_item();
2724  if(NULL != ti) { deselect(ti); }
2725  select(get_item_focus(), 1);
2726 # endif // 10301 <= FL_ABI_VERSION
2727 #endif // FL_ABI_VERSION
2728  res = 1;
2729  break;
2730  }
2731  // Space key
2732  else if(Fl::event_key() == 0x20)
2733  {
2734  mainWindow->ascrolldown_cb(true);
2735  res = 1;
2736  break;
2737  }
2738  // Slash key pressed
2739  else if(!std::strcmp(Fl::event_text(), "/"))
2740  {
2741  mainWindow->asearch_cb_i();
2742  res = 1;
2743  break;
2744  }
2745  // No 'break' here is intended, use default handler if ignored
2746  }
2747  default:
2748  {
2749  res = Fl_Tree::handle(event);
2750  break;
2751  }
2752  }
2753 
2754  return(res);
2755 }
2756 
2757 
2758 // =============================================================================
2759 // Main window callback state machine
2760 //
2761 // This function is intended to check whether an operation is currently allowed
2762 // to execute.
2763 //
2764 // Every operation that use shared ressources (like the core thread) must call
2765 // this function before start of execution with \e operation set to a unique
2766 // operation ID associated with this operation.
2767 // This function returns \c true (and change internal state) if starting
2768 // execution of this operation is allowed at the moment, otherwise the operation
2769 // must not be started (and the internal state is preserved).
2770 //
2771 // Some operations call this function again with a different operation ID to
2772 // indicate completion (such indications are always accepted by this function).
2773 
2774 bool MainWindow::stateMachine(enum mainWindowEvent operation)
2775 {
2776  bool res = true;
2777 
2778  switch(mainState)
2779  {
2780  case STATE_READY:
2781  {
2782  switch(operation)
2783  {
2784  case EVENT_SUBSCRIBE:
2785  case EVENT_GL_REFRESH:
2786  case EVENT_A_VIEW:
2787  case EVENT_A_MAAR:
2788  case EVENT_A_MAGAR:
2789  case EVENT_SRC_VIEW:
2790  case EVENT_MOTD_VIEW:
2791  {
2792  mainState = STATE_MUTEX;
2793  break;
2794  }
2795  case EVENT_SERVER:
2796  {
2797  mainState = STATE_SERVER1;
2798  break;
2799  }
2800  case EVENT_G_SELECT:
2801  {
2802  mainState = STATE_GROUP;
2803  break;
2804  }
2805  case EVENT_A_PREPARE:
2806  {
2807  mainState = STATE_NEXT;
2808  break;
2809  }
2810  case EVENT_SCROLL_NEXT:
2811  {
2812  mainState = STATE_SCROLL;
2813  break;
2814  }
2815  case EVENT_COMPOSE:
2816  {
2817  mainState = STATE_COMPOSE;
2818  break;
2819  }
2820  case EVENT_SERVER_EXIT:
2821  case EVENT_G_SELECT_EXIT:
2822  case EVENT_GL_REFRESH_EXIT:
2823  case EVENT_SCROLL_NEXT_EXIT:
2824  {
2825  // Ignore
2826  break;
2827  }
2828  default:
2829  {
2830  // Reject all other operations
2831  res = false;
2832  break;
2833  }
2834  }
2835  break;
2836  }
2837  case STATE_MUTEX:
2838  {
2839  switch(operation)
2840  {
2841  case EVENT_SUBSCRIBE_EXIT:
2842  case EVENT_GL_REFRESH_EXIT:
2843  case EVENT_A_VIEW_EXIT:
2844  case EVENT_A_MAAR_EXIT:
2845  case EVENT_A_MAGAR_EXIT:
2846  case EVENT_SRC_VIEW_EXIT:
2847  case EVENT_MOTD_VIEW_EXIT:
2848  {
2849  mainState = STATE_READY;
2850  break;
2851  }
2852  default:
2853  {
2854  // Reject all other operations
2855  res = false;
2856  break;
2857  }
2858  }
2859  break;
2860  }
2861  case STATE_SERVER1:
2862  {
2863  switch(operation)
2864  {
2865  case EVENT_GL_PROPOSAL:
2866  {
2867  mainState = STATE_PROPOSAL;
2868  break;
2869  }
2870  case EVENT_GL_REFRESH:
2871  {
2872  mainState = STATE_SERVER2;
2873  break;
2874  }
2875  case EVENT_SERVER_EXIT:
2876  {
2877  mainState = STATE_READY;
2878  break;
2879  }
2880  default:
2881  {
2882  // Reject all other operations
2883  res = false;
2884  break;
2885  }
2886  }
2887  break;
2888  }
2889  case STATE_SERVER2:
2890  {
2891  switch(operation)
2892  {
2893  case EVENT_SERVER_EXIT:
2894  case EVENT_GL_PROPOSAL_EXIT:
2895  {
2896  // Ignore
2897  break;
2898  }
2899  case EVENT_GL_REFRESH_EXIT:
2900  {
2901  mainState = STATE_READY;
2902  break;
2903  }
2904  default:
2905  {
2906  // Reject all other operations
2907  res = false;
2908  break;
2909  }
2910  }
2911  break;
2912  }
2913  case STATE_PROPOSAL:
2914  {
2915  switch(operation)
2916  {
2917  case EVENT_SERVER_EXIT:
2918  {
2919  // Ignore
2920  break;
2921  }
2922  case EVENT_GL_PROPOSAL_EXIT:
2923  {
2924  mainState = STATE_READY;
2925  break;
2926  }
2927  case EVENT_GL_REFRESH:
2928  {
2929  mainState = STATE_SERVER2;
2930  break;
2931  }
2932  default:
2933  {
2934  // Reject all other operations
2935  res = false;
2936  break;
2937  }
2938  }
2939  break;
2940  }
2941  case STATE_GROUP:
2942  {
2943  switch(operation)
2944  {
2945  case EVENT_G_SELECT_EXIT:
2946  case EVENT_AT_REFRESH:
2947  {
2948  // Preserve state
2949  break;
2950  }
2951  case EVENT_AT_REFRESH_EXIT:
2952  {
2953  mainState = STATE_READY;
2954  break;
2955  }
2956  default:
2957  {
2958  // Reject all other operations
2959  res = false;
2960  break;
2961  }
2962  }
2963  break;
2964  }
2965  case STATE_SCROLL:
2966  {
2967  switch(operation)
2968  {
2969  case EVENT_A_PREPARE:
2970  {
2971  mainState = STATE_NEXT;
2972  break;
2973  }
2974  case EVENT_SCROLL_NEXT_EXIT:
2975  {
2976  mainState = STATE_READY;
2977  break;
2978  }
2979  default:
2980  {
2981  // Reject all other operations
2982  res = false;
2983  break;
2984  }
2985  }
2986  break;
2987  }
2988  case STATE_NEXT:
2989  {
2990  switch(operation)
2991  {
2992  case EVENT_A_SELECT:
2993  case EVENT_SCROLL_NEXT_EXIT:
2994  {
2995  // Preserve state
2996  break;
2997  }
2998  case EVENT_A_SELECT_EXIT:
2999  {
3000  mainState = STATE_READY;
3001  break;
3002  }
3003  default:
3004  {
3005  // Reject all other operations
3006  res = false;
3007  break;
3008  }
3009  }
3010  break;
3011  }
3012  case STATE_COMPOSE:
3013  {
3014  switch(operation)
3015  {
3016  case EVENT_POST:
3017  {
3018  mainState = STATE_POST;
3019  break;
3020  }
3021  case EVENT_COMPOSE_EXIT:
3022  {
3023  mainState = STATE_READY;
3024  break;
3025  }
3026  default:
3027  {
3028  // Reject all other operations
3029  res = false;
3030  break;
3031  }
3032  }
3033  break;
3034  }
3035  case STATE_POST:
3036  {
3037  switch(operation)
3038  {
3039  case EVENT_POST_EXIT:
3040  {
3041  mainState = STATE_COMPOSE;
3042  break;
3043  }
3044  default:
3045  {
3046  // Reject all other operations
3047  res = false;
3048  break;
3049  }
3050  }
3051  break;
3052  }
3053  default:
3054  {
3055  // This should never be executed, try to recover
3056  PRINT_ERROR("Invalid state detected by main window state machine");
3057  mainState = STATE_READY;
3058  res = false;
3059  break;
3060  }
3061  }
3062 
3063  return(res);
3064 }
3065 
3066 
3067 // =============================================================================
3068 // Main Window exit callback
3069 
3070 void MainWindow::exit_cb_i(void)
3071 {
3072  // Ignore escape key
3073  if(Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
3074  {
3075  return;
3076  }
3077 
3078  // Schedule exit request
3079  exitRequest = 1;
3080 }
3081 
3082 
3083 // =============================================================================
3084 // Main Window print callback
3085 
3086 void MainWindow::print_cb_i(void)
3087 {
3088  const Fl_Font font = FL_COURIER;
3089  const Fl_Fontsize fontsize = 14;
3090  const char line[] = "________________________________________"
3091  "________________________________________";
3092  int old_scrollbar_size = Fl::scrollbar_size();
3093  int res = 0;
3094  Fl_Printer printer;
3095  Fl_Text_Buffer tb;
3096  Fl_Text_Buffer sb;
3097  Fl_Text_Display canvas(0, 0, 100, 100);
3098  int lines;
3099  int pages = 1;
3100  int frompage, topage;
3101  int w; // Canvas width in points
3102  int h = 0; // Canvas height in points
3103  int lh; // Line hight in points
3104  int lpp = 0; // Lines per page
3105  int pw, ph; // Printable area size in points
3106  float sf; // Scale factor
3107  float tmp;
3108  int i;
3109  char* p;
3110  int x, y;
3111  int current_line = 1; // The first line is number 1
3112  int rv;
3113 
3114  if(NULL == currentGroup)
3115  {
3116  SC("Do not use non-ASCII for the translation of this item")
3117  fl_message_title(S("Error"));
3118  fl_alert("%s", S("No group selected"));
3119  }
3120  else if(NULL == currentArticleHE)
3121  {
3122  SC("Do not use non-ASCII for the translation of this item")
3123  fl_message_title(S("Error"));
3124  fl_alert("%s", S("No article selected"));
3125  }
3126  else
3127  {
3128  // Calculate width for 80 columns and hight for ISO 216 A4 paper
3129  fl_font(font, fontsize);
3130  fl_measure(line, w = 0, lh = 0);
3131  if(0 >= w || 0 >= lh) { res = -1; }
3132  else { h = (int) std::ceil((double) w * std::sqrt(2.0)); }
3133  if(!res)
3134  {
3135  // Prepare text buffer
3136  p = text->buffer()->text();
3137  if(NULL != p) { tb.text(p); }
3138  std::free((void*) p);
3139  lines = tb.count_lines(0, tb.length());
3140  // Prepare style buffer
3141  p = currentStyle->text();
3142  if(NULL != p) { sb.text(p); }
3143  std::free((void*) p);
3144  // Prepare canvas
3145  w += lh;
3146  h += lh;
3147  canvas.box(FL_NO_BOX);
3148  canvas.textfont(font);
3149  canvas.textsize(fontsize);
3150  canvas.resize(0, 0, w, h);
3151  canvas.buffer(&tb);
3152  canvas.highlight_data(&sb, styles, styles_len, 'A', NULL, NULL);
3153  // Calculate number of pages
3154  i = 0;
3155  lpp = 0;
3156  while(canvas.position_to_xy(i, &x, &y))
3157  {
3158  if(!canvas.move_down()) { break; }
3159  i = canvas.insert_position();
3160  ++lpp;
3161  }
3162  if(lines > lpp)
3163  {
3164  // Remove one line because the last one is maybe only partly visible
3165  --lpp; if(0 >= lpp) { res = -1; }
3166  if(!res)
3167  {
3168  for(i = 0; i < lpp; ++i) { tb.append("\n"); } // For scrolling
3169  pages = lines / lpp;
3170  if(lines % lpp) { ++pages; }
3171  if(1 > pages) { res = -1; }
3172  }
3173  }
3174  }
3175  // Setup printer
3176  if(!res)
3177  {
3178  res = printer.start_job(pages, &frompage, &topage);
3179  if(!res)
3180  {
3181  // Scrollbars of canvas are printed without this
3182  Fl::scrollbar_size(0);
3183  for(i = 1; i <= pages; ++i)
3184  {
3185  // Scroll down to next page
3186  canvas.scroll(current_line, 0);
3187  // Check whether page was selected
3188  if(i >= frompage && i <= topage)
3189  {
3190  // Scale page to printable area
3191  res = printer.start_page();
3192  if(!res)
3193  {
3194  res = printer.printable_rect(&pw, &ph);
3195  if(!res)
3196  {
3197  sf = (float) pw / (float) w;
3198  tmp = (float) ph / (float) h;
3199  if(std::fabs(tmp) < std::fabs(sf)) { sf = tmp; }
3200  // Scaling sets origin too
3201  printer.scale(sf);
3202  // Print page
3203  printer.print_widget((Fl_Widget*) &canvas);
3204  }
3205  rv = printer.end_page();
3206  if(!res) { res = rv; }
3207  }
3208  }
3209  // Check for error
3210  if(res) { break; }
3211  else
3212  {
3213  // Calculate vertical offset for next page
3214  current_line += lpp;
3215  }
3216  }
3217  printer.end_job();
3218  Fl::scrollbar_size(old_scrollbar_size);
3219  }
3220  }
3221  }
3222 
3223  if(res)
3224  {
3225  SC("Do not use non-ASCII for the translation of this item")
3226  fl_message_title(S("Error"));
3227  fl_alert("%s", S("Printing failed or aborted"));
3228  }
3229 }
3230 
3231 
3232 // =============================================================================
3233 // Main Window save cooked article (to file, UTF-8 encoded) callback
3234 
3235 void MainWindow::asave_cb_i(void)
3236 {
3237  const char* pathname;
3238  int rv;
3239 
3240  if(NULL == currentGroup)
3241  {
3242  SC("Do not use non-ASCII for the translation of this item")
3243  fl_message_title(S("Error"));
3244  fl_alert("%s", S("No group selected"));
3245  }
3246  else if(NULL == currentArticleHE)
3247  {
3248  SC("Do not use non-ASCII for the translation of this item")
3249  fl_message_title(S("Error"));
3250  fl_alert("%s", S("No article selected"));
3251  }
3252  else
3253  {
3254  SC("Do not use characters for the translation that cannot be")
3255  SC("converted to the ISO 8859-1 character set for this item.")
3256  SC("Leave the original string in place if in doubt.")
3257  const char* title = S("Save article");
3258  const char* suggested_pathname = core_suggest_pathname();
3259 
3260  fl_file_chooser_ok_label(S("Save"));
3261  if(NULL != suggested_pathname)
3262  {
3263  pathname = fl_file_chooser(title, "*", suggested_pathname, 0);
3264  if(NULL != pathname)
3265  {
3266  // Ask user for overwrite permission if file exists
3267  rv = gui_check_pathname(pathname);
3268  if (0 == rv)
3269  {
3270  rv = gui_save_to_file(pathname, currentArticle);
3271  if(rv)
3272  {
3273  SC("Do not use non-ASCII for the translation of this item")
3274  fl_message_title(S("Error"));
3275  fl_alert("%s", S("Operation failed"));
3276  }
3277  }
3278  }
3279  core_free((void*) suggested_pathname);
3280  }
3281  }
3282 }
3283 
3284 
3285 // =============================================================================
3286 // Main Window search in article content
3287 
3288 void MainWindow::asearch_cb_i(void)
3289 {
3290  SearchWindow* sw = NULL;
3291  int result = -1;
3292  int found = 0;
3293  int found_pos = 0;
3294  std::size_t tmp, tmp2;
3295  char* p;
3296  int start, end, len;
3297  int sel_start, sel_end;
3298  SC("")
3299  SC("Do not use characters for the translation that cannot be converted to")
3300  SC("the ISO 8859-1 character set for this item.")
3301  SC("Leave the original string in place if in doubt.")
3302  const char* titleString = S("Search in article");
3303  SC("")
3304 
3305  if(NULL == currentArticleHE)
3306  {
3307  SC("Do not use non-ASCII for the translation of this item")
3308  fl_message_title(S("Error"));
3309  fl_alert("%s", S("No article selected"));
3310  }
3311  else
3312  {
3313  // Display "Search in article" window
3314 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3315  // Convert window title to the encoding required by the window manager (WM)
3316  // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
3317  // window titles.
3318  const char* title;
3319  title = gui_utf8_iso(titleString);
3320  if(NULL != title)
3321  {
3322  sw = new SearchWindow(title, &currentSearchString);
3323  delete[] title;
3324  }
3325 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3326  sw = new SearchWindow(titleString, &currentSearchString);
3327 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3328 
3329  // Wait for user to press OK or Cancel
3330 search_again:
3331  if(NULL != sw)
3332  {
3333  sw->default_cursor(FL_CURSOR_DEFAULT);
3334  UI_READY();
3335  sw->finished = 0;
3336  while(!sw->finished) { Fl::wait(); }
3337  sw->default_cursor(FL_CURSOR_WAIT);
3338  result = sw->finished;
3339  }
3340 
3341  // Check result
3342  currentArticle->unhighlight();
3343  if(0 > result)
3344  {
3345  // Search cancelled by user
3346  UI_STATUS(S("Search cancelled, reset start position."));
3347  currentSearchPosition = 0;
3348  }
3349  else
3350  {
3351  // Search in current article
3352  UI_STATUS(S("Searching in article ..."));
3353  UI_BUSY();
3354  Fl::check();
3355  if(config[CONF_SEARCH_CASE_IS].val.i)
3356  {
3357  // Case insensitive (using FLTK will not be Unicode conformant)
3358  p = currentArticle->text();
3359  found = !enc_uc_search(p, (size_t) currentSearchPosition,
3360  currentSearchString, &tmp, &tmp2);
3361  if((std::size_t) INT_MAX <= tmp) { found_pos = INT_MAX; }
3362  else { found_pos = (int) tmp; }
3363  if((std::size_t) INT_MAX <= tmp2) { len = INT_MAX; }
3364  else { len = (int) tmp2; }
3365  std::free((void*) p);
3366  }
3367  else
3368  {
3369  // Case sensitive (using FLTK)
3370  found = currentArticle->search_forward(currentSearchPosition,
3371  currentSearchString,
3372  &found_pos, 1);
3373  // Length was already checked for INT_MAX overflow by SearchWindow
3374  len = (int) std::strlen(currentSearchString);
3375  }
3376  if(found)
3377  {
3378  // Highlight found text
3379  start = found_pos;
3380  if(INT_MAX - start < len) { len = INT_MAX - start; }
3381  end = start + len;
3382  currentSearchPosition = end;
3383  currentArticle->highlight(start, end);
3384  UI_STATUS(S("Search string found and marked, position stored."));
3385  // Scroll vertically to bring found position into view
3386  //if(currentArticle->selection_position(&sel_start, &sel_end))
3387  if(currentArticle->highlight_position(&sel_start, &sel_end))
3388  {
3389  // Now: Horizontal scrolling doesn't work correctly without this
3390  text->insert_position(0);
3391  text->show_insert_position();
3392  text->insert_position(sel_end);
3393  text->show_insert_position();
3394  }
3395  }
3396  else
3397  {
3398  UI_STATUS(S("Search string not found, reset start position."));
3399  // Reset start position
3400  currentSearchPosition = 0;
3401  }
3402  // Return to search window
3403  goto search_again;
3404  }
3405 
3406  // Destroy search window
3407  if(NULL != sw) { delete sw; }
3408  UI_READY();
3409  }
3410 }
3411 
3412 
3413 // =============================================================================
3414 // Main window calculate percent value and label for progress bar
3415 
3416 void MainWindow::calculatePercent(std::size_t current, std::size_t complete)
3417 {
3418  std::ostringstream percentString;
3419 
3420  progress_percent_value = 100.0;
3421  progress_percent_label[0] = '1';
3422  progress_percent_label[1] = '0';
3423  progress_percent_label[2] = '0';
3424  progress_percent_label[3] = '%';
3425  if(complete && (current < complete))
3426  {
3427  progress_percent_value = (float) current / (float) complete;
3428  progress_percent_value *= (float) 100.0;
3429  percentString.precision(0);
3430  percentString << std::fixed << progress_percent_value << "%"
3431  << std::flush;
3432 
3433  // Attention:
3434  //
3435  // const char* s = percentString.str().c_str()
3436  //
3437  // creates 's' with undefined value because the compiler is allowed to
3438  // free the temporary string object returned by 'str()' immediately after
3439  // the assignment!
3440  // Assigning names to temporary string objects forces them to stay in
3441  // memory as long as their names go out of scope (this is what we need).
3442  const std::string& ps = percentString.str();
3443 
3444  std::strncpy(progress_percent_label, ps.c_str(), (std::size_t) 4);
3445  }
3446  // Terminate string
3447  progress_percent_label[4] = 0;
3448 }
3449 
3450 
3451 // =============================================================================
3452 // Main Window configuration callback
3453 
3454 void MainWindow::config_cb_i(void)
3455 {
3456  SC("")
3457  SC("Do not use characters for the translation that cannot be converted to")
3458  SC("the ISO 8859-1 character set for this item.")
3459  SC("Leave the original string in place if in doubt.")
3460  const char* titleString = S("Configuration");
3461  SC("")
3462 
3463  // Display "Configuration" window
3464 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3465  // Convert window title to the encoding required by the window manager (WM)
3466  // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
3467  // window titles.
3468  const char* title;
3469  title = gui_utf8_iso(titleString);
3470  if(NULL != title)
3471  {
3472  new MiscCfgWindow(title);
3473  delete[] title;
3474  }
3475 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3476  new MiscCfgWindow(titleString);
3477 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3478 }
3479 
3480 
3481 // =============================================================================
3482 // Main Window identity configuration callback
3483 
3484 void MainWindow::identity_cb_i(void)
3485 {
3486  SC("")
3487  SC("Do not use characters for the translation that cannot be converted to")
3488  SC("the ISO 8859-1 character set for this item.")
3489  SC("Leave the original string in place if in doubt.")
3490  const char* titleString = S("Identity configuration");
3491  SC("")
3492 
3493  // Display "Identity configuration" window
3494 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3495  // Convert window title to the encoding required by the window manager (WM)
3496  // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
3497  // window titles.
3498  const char* title;
3499  title = gui_utf8_iso(titleString);
3500  if(NULL != title)
3501  {
3502  new IdentityCfgWindow(title);
3503  delete[] title;
3504  }
3505 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3506  new IdentityCfgWindow(titleString);
3507 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3508 }
3509 
3510 
3511 // =============================================================================
3512 // Main Window Message-ID search callback
3513 
3514 void MainWindow::mid_search_cb_i(void)
3515 {
3516  SC("")
3517  SC("Do not use characters for the translation that cannot be converted to")
3518  SC("the ISO 8859-1 character set for this item.")
3519  SC("Leave the original string in place if in doubt.")
3520  const char* titleString = S("Message-ID search");
3521  SC("")
3522 
3523  // Display "Message-ID search" window
3524 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3525  // Convert window title to the encoding required by the window manager (WM)
3526  // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
3527  // window titles.
3528  const char* title;
3529  title = gui_utf8_iso(titleString);
3530  if(NULL != title)
3531  {
3532  new MIDSearchWindow(title);
3533  delete[] title;
3534  }
3535 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3536  new MIDSearchWindow(titleString);
3537 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3538 }
3539 
3540 
3541 // =============================================================================
3542 // Main Window about information callback
3543 
3544 void MainWindow::about_cb_i(void)
3545 {
3546  std::ostringstream titleString;
3547 
3548  // Create Unicode title for about window
3549  SC("")
3550  SC("Do not use characters for the translation that cannot be converted to")
3551  SC("the ISO 8859-1 character set for this item.")
3552  SC("Leave the original string in place if in doubt.")
3553  titleString << S("About") << " " << CFG_NAME << std::flush;
3554  SC("")
3555 
3556  // Attention:
3557  //
3558  // const char* title = titleString.str().c_str()
3559  //
3560  // creates 'title' with undefined value because the compiler is allowed to
3561  // free the temporary string object returned by 'str()' immediately after the
3562  // assignment!
3563  // Assigning names to temporary string objects forces them to stay in memory
3564  // as long as their names go out of scope (this is what we need).
3565  const std::string& ts = titleString.str();
3566  const std::string& as = aboutString.str();
3567 
3568  // Set "About" window title
3569 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3570  // Convert window title to the encoding required by the window manager (WM)
3571  // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
3572  // window titles.
3573  const char* title;
3574  title = gui_utf8_iso(ts.c_str());
3575  if(NULL != title)
3576  {
3577  fl_message_title(title);
3578  delete[] title;
3579  }
3580 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3581  fl_message_title(ts.c_str());
3582 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3583 
3584  // Display "About" window
3585  fl_message("%s", as.c_str());
3586 }
3587 
3588 
3589 // =============================================================================
3590 // Main Window bug report callback
3591 
3592 void MainWindow::bug_cb_i()
3593 {
3594  std::ostringstream subjectString;
3595  std::ostringstream configString;
3596  std::ostringstream contentString;
3597  std::ostringstream titleString;
3598  int rv = -1;
3599  char* maintainer;
3600  std::size_t len;
3601  char* p;
3602  char* q;
3603 
3604  configString << "Configuration:" << "\n"
3605  << CFG_NAME << " " << CFG_VERSION
3606  << " " << "for" << " " << CFG_OS << "\n"
3607 #if CFG_MODIFIED
3608  << "(This is a modified version!)" << "\n"
3609 #endif // CFG_MODIFIED
3610  << "Unicode version: " << UC_VERSION << "\n"
3611  << "NLS: "
3612 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3613  << "Enabled" << "\n"
3614 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3615  << "Disabled" << "\n"
3616 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3617  << "Message locale: " << nls_loc << "\n"
3618  << "TLS: "
3619 #if CFG_USE_TLS
3620  << "Available" << "\n"
3621 # if CFG_USE_OPENSSL_API_1_1
3622  << "Compiled for OpenSSL API 1.1" << "\n"
3623 # endif // !CFG_USE_OPENSSL_API_1_1
3624 #else // CFG_USE_TLS
3625  << "Not available" << "\n"
3626 #endif // CFG_USE_TLS
3627  << "Compiled for: FLTK " << FL_MAJOR_VERSION << "."
3628  << FL_MINOR_VERSION << "." << FL_PATCH_VERSION << "\n"
3629 #if CFG_CMPR_DISABLE
3630  << "Compression disabled" << "\n"
3631 #else // CFG_CMPR_DISABLE
3632  << "Compression available"
3633 # if CFG_USE_ZLIB
3634  << " (zlib)"
3635 # endif // CFG_USE_ZLIB
3636  << "\n"
3637 #endif // CFG_CMPR_DISABLE
3638  << "Build: " << BDATE
3639  << "\n\n"
3640  << "Problem description (in english):" << "\n\n"
3641  << std::flush;
3642  subjectString << "[" << CFG_NAME << "] "
3643  << "Bug report (...)"
3644  << std::flush;
3645  contentString << S("Report bug to:") << "\n"
3646  << CFG_MAINTAINER
3647  << "\n\n"
3648  << S("Use the following subject line and replace")
3649  << " '...' " << S("with a summary") << ":\n"
3650  << subjectString.str() << "\n\n"
3651  << S("Use the following skeleton:") << "\n"
3652  << "------------------------------------------------------\n"
3653  << configString.str()
3654  << "------------------------------------------------------\n"
3655  << std::flush;
3656 
3657  // Create Unicode title for bug window
3658  SC("")
3659  SC("Do not use characters for the translation that cannot be converted to")
3660  SC("the ISO 8859-1 character set for this item.")
3661  SC("Leave the original string in place if in doubt.")
3662  titleString << S("Bug report") << std::flush;
3663  SC("")
3664 
3665  // Attention:
3666  //
3667  // const char* title = titleString.str().c_str()
3668  //
3669  // creates 'title' with undefined value because the compiler is allowed to
3670  // free the temporary string object returned by 'str()' immediately after
3671  // the assignment!
3672  // Assigning names to temporary string objects forces them to stay in
3673  // memory as long as their names go out of scope (this is what we need).
3674  const std::string& ts = titleString.str();
3675  const std::string& cs = contentString.str();
3676  const std::string& config = configString.str();
3677  const std::string& subject = subjectString.str();
3678 
3679  // Try to start external e-mail handler
3680  len = std::strlen(CFG_MAINTAINER);
3681  p = new char[++len];
3682  std::strncpy(p, CFG_MAINTAINER, len);
3683  p[len - (size_t) 1] = 0; // Not needed, only to silence 'cppcheck'
3684  // Attention: Overloaded and both prototypes different than in C!
3685  q = std::strstr(p, "mailto:");
3686  if(NULL != q)
3687  {
3688  maintainer = &q[7];
3689  rv = enc_percent_decode(maintainer, 1);
3690  if(0 <= rv)
3691  {
3692  rv = ext_handler_email(maintainer, subject.c_str(), config.c_str());
3693  }
3694  }
3695  delete[] p;
3696  if(rv)
3697  {
3698  // Failed => Display information in own window
3699 
3700  // Set "Bug report" window title
3701 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3702  // Convert window title to encoding required by the window manager (WM)
3703  // It is assumed that the WM can display either ISO 8859-1 or UTF-8
3704  // encoded window titles.
3705  const char* title;
3706  title = gui_utf8_iso(ts.c_str());
3707  if(NULL != title)
3708  {
3709  // Display "Bug report" window
3710  new BugreportWindow(title, cs.c_str());
3711  // Release memory for window title
3712  delete[] title;
3713  }
3714 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3715  // Display "Bug report" window
3716  new BugreportWindow(ts.c_str(), cs.c_str());
3717 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3718  }
3719 }
3720 
3721 
3722 // =============================================================================
3723 // Main Window license information callback
3724 
3725 void MainWindow::license_cb_i(void)
3726 {
3727  std::ostringstream titleString;
3728 
3729  // Create Unicode title for license window
3730  SC("")
3731  SC("Do not use characters for the translation that cannot be converted to")
3732  SC("the ISO 8859-1 character set for this item.")
3733  SC("Leave the original string in place if in doubt.")
3734  titleString << S("License") << std::flush;
3735  SC("")
3736 
3737  // Attention:
3738  //
3739  // const char* title = titleString.str().c_str()
3740  //
3741  // creates 'title' with undefined value because the compiler is allowed to
3742  // free the temporary string object returned by 'str()' immediately after the
3743  // assignment!
3744  // Assigning names to temporary string objects forces them to stay in memory
3745  // as long as their names go out of scope (this is what we need).
3746  const std::string& ts = titleString.str();
3747 
3748  // Set "License" window title
3749 #if CFG_USE_XSI && !CFG_NLS_DISABLE
3750  // Convert window title to the encoding required by the window manager (WM)
3751  // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
3752  // window titles.
3753  const char* title;
3754  title = gui_utf8_iso(ts.c_str());
3755  if(NULL != title)
3756  {
3757  // Display "License" window
3758  new LicenseWindow(title);
3759  // Release memory for window title
3760  delete[] title;
3761  }
3762 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
3763  // Display "License" window
3764  new LicenseWindow(ts.c_str());
3765 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
3766 }
3767 
3768 
3769 // =============================================================================
3770 // Main Window protocol console callback
3771 
3772 void MainWindow::console_cb_i(void)
3773 {
3774  if(NULL == protocolConsole)
3775  {
3776  SC("")
3777  SC("Do not use characters for the translation that cannot be converted to")
3778  SC("the ISO 8859-1 character set for this item.")
3779  SC("Leave the original string in place if in doubt.")
3780  protocolConsole = new ProtocolConsole(S("Protocol console"));
3781  SC("")
3782  }
3783 }
3784 
3785 
3786 // =============================================================================
3787 // Main Window ROT13 encode/decode callback
3788 
3789 void MainWindow::rot13_cb_i(void)
3790 {
3791  char* olddata;
3792  Fl_Text_Buffer* newdata = new Fl_Text_Buffer(0, 0);
3793 
3794  if(NULL == currentGroup)
3795  {
3796  SC("Do not use non-ASCII for the translation of this item")
3797  fl_message_title(S("Error"));
3798  fl_alert("%s", S("No group selected"));
3799  }
3800  else if(NULL == currentArticleHE)
3801  {
3802  SC("Do not use non-ASCII for the translation of this item")
3803  fl_message_title(S("Error"));
3804  fl_alert("%s", S("No article selected"));
3805  }
3806  else
3807  {
3808  // ROT13 should not be applied twice
3809  // The clickable references will not work anymore in this case
3810  if(rot13)
3811  {
3812  // Reload article
3813  aselect_cb_i();
3814  rot13 = false;
3815  }
3816  else
3817  {
3818  // Apply ROT13
3819  olddata = currentArticle->text();
3820  if(NULL != olddata)
3821  {
3822  enc_rot13(olddata);
3823  newdata->text(olddata);
3824  articleUpdate(newdata);
3825  }
3826  std::free((void*) olddata);
3827  rot13 = true;
3828  }
3829  }
3830 }
3831 
3832 
3833 // =============================================================================
3834 // Main Window mark single article unread callback
3835 // Now used to toggle (mark read if called again)
3836 
3837 void MainWindow::msau_cb_i(void)
3838 {
3839  Fl_Tree_Item* ti;
3840  core_anum_t a;
3841 
3842  if(NULL == currentGroup)
3843  {
3844  SC("Do not use non-ASCII for the translation of this item")
3845  fl_message_title(S("Error"));
3846  fl_alert("%s", S("No group selected"));
3847  }
3848  else if(NULL == currentArticleHE)
3849  {
3850  SC("Do not use non-ASCII for the translation of this item")
3851  fl_message_title(S("Error"));
3852  fl_alert("%s", S("No article selected"));
3853  }
3854  else
3855  {
3856  ti = articleTree->first_selected_item();
3857  a = ((core_hierarchy_element*) ti->user_data())->anum;
3858  // Check whether article was already read
3859  if(core_check_already_read(&group_list[group_list_index],
3860  (core_hierarchy_element*) ti->user_data()))
3861  {
3862  // Yes => Mark unread
3863  ti->labelfont(FL_HELVETICA_BOLD);
3864  core_mark_as_unread(&group_list[group_list_index], a);
3865  UI_STATUS(S("Marked article unread."));
3866  }
3867  else
3868  {
3869  // No => Mark read
3870  ti->labelfont(FL_HELVETICA);
3871  core_mark_as_read(&group_list[group_list_index], a);
3872  UI_STATUS(S("Marked article read."));
3873  }
3874  articleTree->redraw();
3875  groupListUpdateEntry(group_list_index);
3876  }
3877 }
3878 
3879 
3880 // =============================================================================
3881 // Main Window mark subthread read callback
3882 
3883 void MainWindow::mssar(Fl_Tree_Item* ti)
3884 {
3885  core_anum_t a;
3886  int i;
3887 
3888  a = ((core_hierarchy_element*) ti->user_data())->anum;
3889  // Check whether article was already read
3890  if(!core_check_already_read(&group_list[group_list_index],
3891  (core_hierarchy_element*) ti->user_data()))
3892  {
3893  // No => Mark read
3894  ti->labelfont(FL_HELVETICA);
3895  core_mark_as_read(&group_list[group_list_index], a);
3896  }
3897  // Process children recursively
3898  for(i = 0; ti->children() > i; ++i)
3899  {
3900  mssar(ti->child(i));
3901  }
3902 }
3903 
3904 
3905 void MainWindow::mssar_cb_i(void)
3906 {
3907  Fl_Tree_Item* ti;
3908 
3909  if(NULL == currentGroup)
3910  {
3911  SC("Do not use non-ASCII for the translation of this item")
3912  fl_message_title(S("Error"));
3913  fl_alert("%s", S("No group selected"));
3914  }
3915  else
3916  {
3917  ti = articleTree->first_selected_item();
3918  if(NULL == currentArticleHE || NULL == ti)
3919  {
3920  SC("Do not use non-ASCII for the translation of this item")
3921  fl_message_title(S("Error"));
3922  fl_alert("%s", S("No article selected"));
3923  }
3924  else
3925  {
3926  // Process all article tree nodes in subthread
3927  // Starting at current article
3928  mssar(ti);
3929 
3930  // Update article tree
3931  updateTree();
3932 
3933  // Close subtree (now marked read)
3934  ti = articleTree->first_selected_item();
3935  if(NULL != ti)
3936  {
3937  articleTree->close(ti);
3938  }
3939 
3940  // Update group list
3941  groupListUpdateEntry(group_list_index);
3942  UI_STATUS(S("Marked all articles in subthread read."));
3943  }
3944  }
3945 }
3946 
3947 
3948 // =============================================================================
3949 // Main Window mark all read callback
3950 
3951 void MainWindow::maar_cb_i(void)
3952 {
3953  Fl_Tree_Item* ti;
3954  core_anum_t a;
3955 
3956  if(stateMachine(EVENT_A_MAAR))
3957  {
3958  if(NULL != currentGroup)
3959  {
3960  // Process all article tree nodes (skip root node)
3961  for(ti = articleTree->first()->next(); ti; ti = articleTree->next(ti))
3962  {
3963  ti->labelfont(FL_HELVETICA);
3964  }
3965  articleTree->redraw();
3966 
3967  // Mark all articles read
3968  // Note: CAC was already applied by group selection CB
3969  if(currentGroup->lwm && currentGroup->hwm >= currentGroup->lwm)
3970  {
3971  // Group is not empty
3972  for(a = currentGroup->hwm; a >= currentGroup->lwm; --a)
3973  {
3974  core_mark_as_read(&group_list[group_list_index], a);
3975  }
3976  }
3977 
3978  // Update group list
3979  groupListUpdateEntry(group_list_index);
3980  UI_STATUS(S("Marked all articles in group read."));
3981  }
3982 
3983  if(!stateMachine(EVENT_A_MAAR_EXIT))
3984  {
3985  PRINT_ERROR("Error in main window state machine");
3986  }
3987  }
3988 }
3989 
3990 
3991 // =============================================================================
3992 // Main Window mark (all articles in) all groups read callback
3993 
3994 void MainWindow::magar_cb_i(void)
3995 {
3996  Fl_Tree_Item* ti;
3997  std::size_t i;
3998  core_groupdesc* g;
3999  core_anum_t a;
4000  int rv;
4001 
4002  if(stateMachine(EVENT_A_MAGAR))
4003  {
4004  // Ask for confirmation
4005  fl_message_title(S("Warning"));
4006  rv = fl_choice("%s", S("No"),
4007  S("Yes"), NULL,
4008  S("Really mark all groups read?"));
4009  if(rv)
4010  {
4011  // Process all article tree nodes in current group (skip root node)
4012  if(NULL != currentGroup)
4013  {
4014  for(ti = articleTree->first()->next(); ti;
4015  ti = articleTree->next(ti))
4016  {
4017  ti->labelfont(FL_HELVETICA);
4018  }
4019  articleTree->redraw();
4020  }
4021 
4022  // Loop over all groups
4023  for(i = 0; group_num > i; ++i)
4024  {
4025  // Mark all articles read
4026  g = &subscribedGroups[i];
4027  if(g->lwm && g->hwm >= g->lwm)
4028  {
4029  // Group is not empty
4030  for(a = g->hwm; a >= g->lwm; --a)
4031  {
4032  core_mark_as_read(&group_list[i], a);
4033  }
4034  }
4035  // Update corresponding group list entry
4036  groupListUpdateEntry(i);
4037  }
4038  UI_STATUS(S("Marked all articles in all groups read."));
4039  }
4040 
4041  if(!stateMachine(EVENT_A_MAGAR_EXIT))
4042  {
4043  PRINT_ERROR("Error in main window state machine");
4044  }
4045  }
4046 }
4047 
4048 
4049 // =============================================================================
4050 // Main Window article tree callback
4051 
4052 void MainWindow::aselect_cb_i(void)
4053 {
4054  Fl_Tree_Item* ti = articleTree->callback_item();
4055 
4056  switch(articleTree->callback_reason())
4057  {
4058  case FL_TREE_REASON_OPENED:
4059  {
4060  // Check whether article was already read
4061  if(core_check_already_read(&group_list[group_list_index],
4062  (core_hierarchy_element*) ti->user_data()))
4063  {
4064  // Yes => Show selected article with normal font style
4065  ti->labelfont(FL_HELVETICA);
4066  }
4067  // Scroll tree so that opened item is on top
4068  scrollTree(UI_SCROLL_TOP, ti);
4069  break;
4070  }
4071  case FL_TREE_REASON_CLOSED:
4072  {
4073  // Check whether tree item has unread children
4074  if(checkTreeBranchForUnread(ti))
4075  {
4076  // Yes => Show selected article with bold font style
4077  ti->labelfont(FL_HELVETICA_BOLD);
4078  }
4079  break;
4080  }
4081  case FL_TREE_REASON_SELECTED:
4082  {
4083 #if 1
4084  // Workaround for unexpected callback with FL_TREE_REASON_SELECTED:
4085  // Sometimes FLTK create such callbacks even for deactivated entries
4086  if(NULL == ti->user_data())
4087  {
4088  PRINT_ERROR("No data associated with selected article item");
4089  break;
4090  }
4091 #endif
4092  // Check whether operation is allowed at the moment
4093  if(stateMachine(EVENT_A_PREPARE))
4094  {
4095  // Check whether tree item has unread children
4096  if(ti->is_open() || !checkTreeBranchForUnread(ti))
4097  {
4098  // Show selected article with normal font (marked read)
4099  // if there are no unread children or they are open/visible
4100  ti->labelfont(FL_HELVETICA);
4101  }
4102  // Store former article HE
4103  lastArticleHE = currentArticleHE;
4104  // Set current article hierarchy element to corresponding article
4105  currentArticleHE = (core_hierarchy_element*) ti->user_data();
4106  // Display corresponding article in content window (use own locking)
4107  articleSelect(UI_CB_START);
4108  // Scroll tree, so that selected item is in the middle
4109  scrollTree(UI_SCROLL_MIDDLE, ti);
4110  // Store selected item so that it can be restored
4111  // (this stored state is used later if the operation is not allowed)
4112  articleTree->store_current(ti);
4113  }
4114  else
4115  {
4116  // Restore former state if operation is not allowed
4117  articleTree->select_former();
4118  }
4119  break;
4120  }
4121  default:
4122  {
4123  break;
4124  }
4125  }
4126 }
4127 
4128 
4129 // =============================================================================
4130 // Main window strip 'angle-addr' token of 'From' header field
4131 //
4132 // The data is modified inside the buffer pointed to by 'from'.
4133 
4134 void MainWindow::stripAngleAddress(char* from)
4135 {
4136  char* p;
4137  char* q;
4138  std::size_t i;
4139  bool strip = false;
4140 
4141  // Attention: Overloaded and both prototypes different than in C!
4142  p = std::strrchr(from, (int) '<');
4143  // Attention: Overloaded and both prototypes different than in C!
4144  q = std::strrchr(from, (int) '>');
4145  if(NULL != p && NULL != q && q > p + 1)
4146  {
4147  // 'angle-addr' token present => Check for 'display-name'
4148  for(i = 0; i < (std::size_t) (p - from); ++i)
4149  {
4150  if(' ' != from[i]) { strip = true; break; }
4151  }
4152  if(strip)
4153  {
4154  // Strip 'angle-addr' and potential space before it
4155  if(' ' == *(p - 1)) { --p; }
4156  *p = 0;
4157  }
4158  else
4159  {
4160  // Extract 'addr-spec'
4161  *q = 0;
4162  std::memmove((void*) from, (void*) (p + 1), (std::size_t) (q - p));
4163  }
4164  }
4165 }
4166 
4167 
4168 // =============================================================================
4169 // Main window reply by e-mail callback
4170 
4171 void MainWindow::sendEmail(void)
4172 {
4173  const char* recipient;
4174  const char* subject;
4175  const char* body = NULL;
4176  int rv;
4177  int invalid = 0;
4178  int warn = 0;
4179  Fl_Text_Buffer tb;
4180  const char* p;
4181  char* q = NULL;
4182  char* r = NULL;
4183  char* s = NULL;
4184  char* t = NULL;
4185  std::size_t i = 0;
4186  char* sig_delim;
4187  char* sig = NULL;
4188  struct core_article_header* hdr = NULL;
4189  char* from = NULL;
4190  std::size_t len;
4191 
4192  if(NULL == currentGroup)
4193  {
4194  SC("Do not use non-ASCII for the translation of this item")
4195  fl_message_title(S("Error"));
4196  fl_alert("%s", S("No group selected"));
4197  }
4198  else if(NULL == currentArticleHE)
4199  {
4200  SC("Do not use non-ASCII for the translation of this item")
4201  fl_message_title(S("Error"));
4202  fl_alert("%s", S("No article selected"));
4203  }
4204  else
4205  {
4206  recipient = currentArticleHE->header->reply2;
4207  if(NULL == recipient) { recipient = currentArticleHE->header->from; }
4208  if(NULL == recipient)
4209  {
4210  // This should never happen because the core should provide strings for
4211  // all mandatory header fields.
4212  PRINT_ERROR("No e-mail address found (bug)");
4213  }
4214  else
4215  {
4216  // Check address of recipient
4217  recipient = enc_extract_addr_spec(recipient);
4218  if(NULL == recipient) { invalid = 1; }
4219  else
4220  {
4221  // Attention: Overloaded and both prototypes different than in C!
4222  p = std::strrchr(recipient, (int) '.');
4223  if(p)
4224  {
4225  // Check for reserved top level domains according to RFC 2606
4226  if(!std::strcmp(p, ".test")) { invalid = 1; }
4227  if(!std::strcmp(p, ".example")) { invalid = 1; }
4228  if(!std::strcmp(p, ".invalid")) { invalid = 1; }
4229  if(!std::strcmp(p, ".localhost")) { invalid = 1; }
4230  }
4231  if(!invalid)
4232  {
4233  // Cite article
4234  hdr = currentArticleHE->header;
4235  t = currentArticle->text();
4236  if(NULL != t)
4237  {
4238  // Strip header
4239  while('_' != t[i++]);
4240  while('|' != t[i++]);
4241  while('|' != t[i++]);
4242  q = &t[++i];
4243  // Strip signature
4244  r = q;
4245  while(1)
4246  {
4247  // Attention:
4248  // Overloaded and both prototypes different than in C!
4249  sig_delim = std::strstr(r, "\n-- \n");
4250  if(NULL == sig_delim) { break; }
4251  else { sig = r = &sig_delim[1]; }
4252  }
4253  if(NULL != sig) { sig[0] = 0; }
4254  // Extract authors 'display-name' and strip 'angle-addr'
4255  len = std::strlen(hdr->from);
4256  from = new char[++len];
4257  std::strcpy(from, hdr->from);
4258  stripAngleAddress(from);
4259  tb.insert(0, q);
4260  gui_cite_content(&tb, from, hdr->groups);
4261  body = tb.text();
4262  delete[] from;
4263  // Do not convert body to canonical form here!
4264  }
4265  // Create subject
4266  subject = currentArticleHE->header->subject;
4267  s = new char[std::strlen(subject) + (std::size_t) 5];
4268  std::strcpy(s, "Re: ");
4269  std::strcat(s, gui_check_re_prefix(subject));
4270  // Attention: Overloaded and both prototypes different than in C!
4271  q = std::strstr(s, "(was:");
4272  if(q && q != s)
4273  {
4274  // Attention:
4275  // Overloaded and both prototypes different than in C!
4276  if(std::strchr(q, (int) ')'))
4277  {
4278  *q = 0;
4279  if(' ' == *(--q)) { *q = 0; }
4280  }
4281  }
4282  subject = s;
4283  // Open warning popup if subject or body contains apostroph chars
4284  if(NULL != subject)
4285  {
4286  // Attention:
4287  // Overloaded and both prototypes different than in C!
4288  if(std::strchr(subject, 0x27)) { warn = 1; }
4289  }
4290  if(!warn && NULL != body)
4291  {
4292  // Attention:
4293  // Overloaded and both prototypes different than in C!
4294  if(std::strchr(body, 0x27)) { warn = 1; }
4295  }
4296  if(warn)
4297  {
4298  SC("Do not use non-ASCII for the translation of this item")
4299  fl_message_title(S("Note"));
4300  fl_message("%s",
4301  S("APOSTROPHE converted to\nRIGHT SINGLE QUOTATION MARK"));
4302  }
4303  // Start external e-mail handler
4304  rv = ext_handler_email(recipient, subject, body);
4305  if(rv)
4306  {
4307  SC("Do not use non-ASCII for the translation of this item")
4308  fl_message_title(S("Error"));
4309  fl_alert("%s", S("Starting e-mail client failed"));
4310  }
4311  delete[] s;
4312  std::free((void*) body);
4313  std::free((void*) t);
4314  }
4315  enc_free((void*) recipient);
4316  }
4317  if(invalid)
4318  {
4319  SC("Do not use non-ASCII for the translation of this item")
4320  fl_message_title(S("Error"));
4321  fl_alert("%s", S("Invalid e-mail address"));
4322  }
4323  }
4324  }
4325 }
4326 
4327 
4328 // =============================================================================
4329 // Main Window scroll down article content and skip to next article
4330 // (must be locked)
4331 
4332 void MainWindow::ascrolldown_cb(bool scroll)
4333 {
4334  int r; // Number of rows
4335  int f = -1; // First row
4336  int l = -1; // Last row
4337  int p = 0; // Position in text buffer
4338  int riv = 0; // Number of rows currently in view
4339  int scrollto;
4340  int x, y;
4341  int i;
4342  bool eoa = true;
4343  Fl_Tree_Item* fi;
4344  Fl_Tree_Item* ti;
4345  Fl_Tree_Item* oi;
4346  bool abort = false;
4347  bool wrap = false;
4348  bool found = false;
4349 
4350  // Check whether operation is allowed at the moment
4351  if(stateMachine(EVENT_SCROLL_NEXT))
4352  {
4353  // Scroll down if requested
4354  if(scroll)
4355  {
4356  // Calculate number of rows
4357  if(NULL != currentArticle && text->buffer() == currentArticle)
4358  {
4359  r = text->count_lines(0, text->buffer()->length(), true);
4360  if(0 < r)
4361  {
4362  for(i = 0; i < r; ++i)
4363  {
4364  if(text->position_to_xy(p, &x, &y))
4365  {
4366  if(0 > f) { l = f = i; }
4367  else { l = i; }
4368  }
4369  p = text->skip_lines(p, 1, true);
4370  }
4371  if(0 > f) { f = 0; l = 0; }
4372  currentLine = f;
4373  riv = l - f + 1;
4374  }
4375 #if 0
4376  // For debugging
4377  std::printf("-----------------------------\n");
4378  std::printf("Rows : %d\n", r);
4379  std::printf("Current row : %d\n", currentLine);
4380  std::printf("First in view: %d\n", f);
4381  std::printf("Last in view : %d\n", l);
4382  std::printf("Rows in view : %d\n", riv);
4383 #endif
4384 
4385  // Scroll down so that second last row will become first in view
4386  if(r && riv)
4387  {
4388  if(r - 1 != l)
4389  {
4390  scrollto = currentLine + riv - 2;
4391  if(scrollto < r - 1)
4392  {
4393  // std::printf("scrollto: %d\n", scrollto);
4394  text->scroll(scrollto + 1, 0);
4395  eoa = false;
4396  }
4397  }
4398  }
4399  }
4400  }
4401 
4402  // Skip to next unread article if at the end of current article
4403  if(eoa)
4404  {
4405  // Start with selected article (or first article if there is none)
4406  fi = articleTree->first_selected_item();
4407  if(NULL == fi)
4408  {
4409  fi = articleTree->first();
4410  if(fi == articleTree->root()) { fi = articleTree->next(fi); }
4411  }
4412  // Check for dummy article
4413  if(NULL == fi->user_data()) { fi = NULL; }
4414  ti = fi;
4415  while(NULL != ti)
4416  {
4417  if(!core_check_already_read(&group_list[group_list_index],
4418  (core_hierarchy_element*) ti->user_data()))
4419  {
4420  // Open branch with next article
4421  oi = ti;
4422  while(articleTree->root() != oi)
4423  {
4424  if(!articleTree->open(oi)) { articleTree->open(oi, 1); }
4425  for(i = 0; i < articleTree->root()->children(); ++i)
4426  {
4427  if(articleTree->root()->child(i) == oi)
4428  {
4429  abort = true;
4430  break;
4431  }
4432  }
4433  if(abort) { break; }
4434  oi = articleTree->prev(oi);
4435  }
4436  // Select next article
4437  articleTree->deselect(fi, 0);
4438  articleTree->set_item_focus(ti);
4439  articleTree->select(ti, 1);
4440  found = true;
4441  break;
4442  }
4443  ti = articleTree->next(ti);
4444  if(NULL == ti && !wrap)
4445  {
4446  // Wrap to beginning once if no article was found yet
4447  wrap = true;
4448  ti = articleTree->first();
4449  if(ti == articleTree->root()) { ti = articleTree->next(ti); }
4450  }
4451  }
4452  }
4453  if(!stateMachine(EVENT_SCROLL_NEXT_EXIT))
4454  {
4455  PRINT_ERROR("Error in main window state machine");
4456  }
4457 
4458  // Sip to next group with unread articles (if no article was found)
4459  if (eoa && !found)
4460  {
4461  if (config[CONF_UNREAD_IN_NEXT_GROUP].val.i) { nug_cb_i(); }
4462  }
4463  }
4464 }
4465 
4466 
4467 // =============================================================================
4468 // Main Window server configuration update
4469 // (must be locked)
4470 
4471 void MainWindow::updateServer(int action)
4472 {
4473  SC("")
4474  SC("Do not use characters for the translation that cannot be converted to")
4475  SC("the ISO 8859-1 character set for this item.")
4476  SC("Leave the original string in place if in doubt.")
4477  const char* titleString = S("Server configuration");
4478  SC("")
4479  int rv = -1;
4480  int cancel = 0;
4481  ServerCfgWindow* scw = NULL;
4482  ServerConfig* sc = NULL;
4483  const char* user = NULL;
4484  const char* pass = NULL;
4485 
4486  // Start core to do the work
4487  if(UI_CB_START == action)
4488  {
4489  // Check whether operation is allowed at the moment
4490  if(!stateMachine(EVENT_SERVER)) { rv = 1; }
4491  else
4492  {
4493  // Display warning message
4494  data.data = NULL;
4495  SC("Do not use non-ASCII for the translation of this item")
4496  fl_message_title(S("Note"));
4497  SC("Line breaks are inserted with \n")
4498  fl_message("%s",
4499  S("Warning:\nGroup states are lost if the server is changed."));
4500 
4501  // Create object with current server configuration
4502  sc = new ServerConfig;
4503  sc->serverReplace(config[CONF_SERVER].val.s);
4504  sc->serviceReplace(config[CONF_SERVICE].val.s);
4505  sc->enc = config[CONF_ENC].val.i;
4506  sc->auth = config[CONF_AUTH].val.i;
4507 
4508  // Display "Server configuration" window
4509 #if CFG_USE_XSI && !CFG_NLS_DISABLE
4510  // Convert window title to the encoding required by the window manager
4511  // (WM). It is assumed that the WM can display either ISO 8859-1 or
4512  // UTF-8 encoded window titles.
4513  const char* title;
4514  title = gui_utf8_iso(titleString);
4515  if(NULL != title)
4516  {
4517  scw = new ServerCfgWindow(sc, title);
4518  delete[] title;
4519  }
4520 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
4521  scw = new ServerCfgWindow(sc, titleString);
4522 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
4523  if(NULL == scw)
4524  {
4525  // This should never happen
4526  PRINT_ERROR("Fatal error while creating server config window");
4527  exitRequest = 1;
4528  }
4529  else
4530  {
4531  if(0 < scw->process())
4532  {
4533  // Start operation
4534  UI_STATUS(S("Update server configuration ..."));
4535  UI_BUSY();
4536  core_mutex_lock();
4537  data.data = (void*) sc;
4539  // Disconnect so that changes take effect
4540  core_disconnect();
4541  // Export current groups (this creates groupfile if not present)
4542  rv = core_export_group_states(group_num, group_list);
4543  if(!rv)
4544  {
4545  // Check whether server name has changed
4546  if(std::strcmp(config[CONF_SERVER].val.s, sc->server))
4547  {
4548  // Reset states in groupfile and delete header database
4549  rv = core_reset_group_states(UI_CB_COOKIE_SERVER);
4550  }
4551  }
4552  }
4553  else { cancel = 1; }
4554  delete scw;
4555  }
4556  }
4557  }
4558  else
4559  {
4560  // Get result
4561  core_mutex_lock();
4562  rv = data.result;
4564  }
4565 
4566  // Check return value (positive value means "in progress")
4567  if(0 >= rv)
4568  {
4569  if(0 > rv)
4570  {
4571  if (!cancel)
4572  {
4573  // Operation failed
4574  UI_STATUS(S("Updating server configuration failed."));
4575  UI_READY();
4576  }
4577  }
4578  else
4579  {
4580  // Restore pointer to server string
4581  core_mutex_lock();
4582  sc = (ServerConfig*) data.data;
4584  // Replace server name in configuration
4585  // (This must be done after the header database was reset and ready for
4586  // the new server)
4587  conf_string_replace(&config[CONF_SERVER], sc->server);
4588  conf_string_replace(&config[CONF_SERVICE], sc->service);
4589  config[CONF_ENC].val.i = sc->enc;
4590  config[CONF_AUTH].val.i = sc->auth;
4591  if(UI_AUTH_USER == sc->auth)
4592  {
4593  // Ask for account name
4594  user = fl_input("%s", config[CONF_USER].val.s, "Login:");
4595  if(NULL != user) { conf_string_replace(&config[CONF_USER], user); }
4596  // Ask for password
4597  SC("Do not use non-ASCII for the translation of this item")
4598  fl_message_title(S("Note"));
4599  rv = fl_choice("%s", S("No"),
4600  S("Yes"), NULL,
4601  S("Store the password from the subsequent request?"));
4602  if(rv) { conf_ephemeral_passwd = 0; }
4603  else { conf_ephemeral_passwd = 1; }
4604  do
4605  {
4606  pass = fl_password("%s", config[CONF_PASS].val.s, "Password:");
4607  if(NULL != pass)
4608  {
4610  if(!config[CONF_PASS].val.s[0])
4611  {
4612  SC("Do not use non-ASCII for the translation of this item")
4613  fl_message_title(S("Warning"));
4614  fl_alert("%s", S("Empty password is not supported"));
4615  }
4616  }
4617  } while(NULL == pass || !config[CONF_PASS].val.s[0]);
4618  }
4619  // Reset CRL update interval back to epoch (force new CRLs)
4620  conf_string_replace(&config[CONF_CRL_UPD_TS], "1970-01-01T00:00:00Z");
4621  // Check for empty group list
4622  if(!group_num)
4623  {
4624  // Ask for password
4625  SC("Do not use non-ASCII for the translation of this item")
4626  fl_message_title(S("Note"));
4627  rv = fl_choice("%s", S("No"),
4628  S("Yes"), NULL,
4629  S("Ask server for group subscription proposals?"));
4630  if(rv)
4631  {
4632  // Ask server for subscription proposals
4633  groupListGetProposal(UI_CB_START);
4634  }
4635  else
4636  {
4637  UI_STATUS(S("No groups subscribed yet."));
4638  UI_READY();
4639  }
4640  }
4641  else
4642  {
4643  // Recreate group list
4644  core_destroy_subscribed_group_states(&group_num, &group_list);
4645  groupListRefresh(UI_CB_START);
4646  }
4647  }
4648  if(NULL != sc) { delete sc; }
4649  if(!stateMachine(EVENT_SERVER_EXIT))
4650  {
4651  PRINT_ERROR("Error in main window state machine");
4652  }
4653  }
4654 }
4655 
4656 
4657 // =============================================================================
4658 // Main window group subscription
4659 // (must be locked)
4660 
4661 void MainWindow::groupSubscribe(int action)
4662 {
4663  int rv = -1;
4664  std::size_t labelcount;
4665  core_grouplabel* labels;
4666  std::size_t i, ii, j;
4667  char* name;
4668  const char* label;
4669  int free_label;
4670  core_anum_t num;
4671  int fake_empty_response = 0;
4672 
4673  // Start core to do the work
4674  if(UI_CB_START == action)
4675  {
4676  if(!stateMachine(EVENT_SUBSCRIBE)) { rv = 1; }
4677  else
4678  {
4679  // Start operation
4680  UI_STATUS(S("Receiving group list ..."));
4681  UI_BUSY();
4682  rv = core_get_group_list(UI_CB_COOKIE_GROUPLIST);
4683  // Note: This will generate callback with action UI_CB_CONTINUE
4684  }
4685  }
4686  else
4687  {
4688  // Get result
4689  core_mutex_lock();
4690  rv = data.result;
4692  }
4693 
4694  if(!rv && UI_CB_CONTINUE == action)
4695  {
4696  core_mutex_lock();
4697  groupcount = data.size;
4698  grouplist = (core_groupdesc*) data.data;
4700  // Start next operation
4701  UI_STATUS(S("Receiving group labels ..."));
4702  UI_BUSY();
4703  rv = core_get_group_labels(UI_CB_COOKIE_GROUPLABELS);
4704  // Note: This will generate callback with action UI_CB_FINISH
4705  if(0 > rv) { fake_empty_response = 1; }
4706  }
4707 
4708  if(0 > rv && UI_CB_FINISH == action) { fake_empty_response = 1; }
4709 
4710  if (fake_empty_response)
4711  {
4712  // Fake valid empty response
4713  core_mutex_lock();
4714  data.size = 0;
4715  data.data = NULL;
4717  rv = 0;
4718  }
4719 
4720  // Check return value (positive value means "in progress")
4721  if(0 >= rv)
4722  {
4723  if(0 > rv)
4724  {
4725  // Operation failed
4726  UI_STATUS(S("Downloading group information failed."));
4727  UI_READY();
4728  }
4729  else
4730  {
4731  // Operation finished
4732  core_mutex_lock();
4733  labelcount = data.size;
4734  labels = (core_grouplabel*) data.data;
4736  UI_STATUS(S("Group information received."));
4737  // Create subscribe window
4738  SC("")
4739  SC("Do not use characters for the translation that cannot be")
4740  SC("converted to the ISO 8859-1 character set for this item.")
4741  SC("Leave the original string in place if in doubt.")
4742  subscribeWindow = new SubscribeWindow(S("Subscribe"),
4743  grouplist, labels);
4744  SC("")
4745  // Add groups
4746  UI_STATUS(S("Process group information ..."));
4747  subscribeWindow->hide();
4748  Fl::wait();
4749  for(i = 0; i < groupcount; ++i)
4750  {
4751  free_label = 0;
4752  // Do not remove this check until you know what you are doing!
4753  if(CORE_GROUP_FLAG_ASCII & grouplist[i].flags)
4754  {
4755  // Search for label
4756  name = grouplist[i].name;
4757  num = grouplist[i].eac;
4758  label = NULL;
4759  for(j = 0; j < labelcount; ++j)
4760  {
4761  if(!strcmp(labels[j].name, name))
4762  {
4763  // Group desciption label found
4764  // Verify UTF-8 encoding of label
4765  if(0 == enc_uc_check_utf8(labels[j].label))
4766  {
4767  // Valid
4768  label = labels[j].label;
4769  }
4770  else
4771  {
4772  // Not valid => Repair UTF-8 encoding
4773  PRINT_ERROR("Invalid encoding of group description");
4774  label = enc_uc_repair_utf8(labels[j].label);
4775  if(NULL != label) { free_label = 1; }
4776  }
4777  break;
4778  }
4779  }
4780  // Replace dots with slashes to create tree hierarchy
4781  ii = 0;
4782  do { if('.' == name[ii]) { name[ii] = '/'; } }
4783  while(name[ii++]);
4784  subscribeWindow->add(name, num, label);
4785  if(free_label) { core_free((void*) label); }
4786  }
4787  }
4788  // Collapse all branches of tree (and append labels, if present)
4789  // Must be the last operation after all groups are added
4790  subscribeWindow->collapseAll();
4791  subscribeWindow->show();
4792  UI_STATUS(S("Group information processed."));
4793  UI_READY();
4794  // The destructor of 'subscribeWindow' will release the memory for
4795  // 'grouplist' and 'labels'. Because this window is modal, no other
4796  // operations are possible in the meanwhile.
4797  }
4798  if(!stateMachine(EVENT_SUBSCRIBE_EXIT))
4799  {
4800  PRINT_ERROR("Error in main window state machine");
4801  }
4802  }
4803 }
4804 
4805 
4806 // =============================================================================
4807 // Main window calculate number of unread articles in group
4808 
4809 core_anum_t MainWindow::groupGetUnreadNo(core_anum_t lwm, core_anum_t hwm,
4810  struct core_range* info)
4811 {
4812  core_anum_t res = 0;
4813  core_anum_t i;
4814 
4815  if(lwm && hwm && hwm >= lwm)
4816  {
4817  res = hwm - lwm + (core_anum_t) 1;
4818  }
4819 
4820  if(res)
4821  {
4822  while(NULL != info)
4823  {
4824  for(i = info->last; i >= info->first; --i)
4825  {
4826  if(i >= lwm && i <= hwm) { if(res) { --res; } }
4827  }
4828  info = info->next;
4829  }
4830  }
4831 
4832  return(res);
4833 }
4834 
4835 
4836 // =============================================================================
4837 // Main window update entry i in group list
4838 
4839 void MainWindow::groupListUpdateEntry(std::size_t i)
4840 {
4841  std::ostringstream groupString;
4842  core_anum_t eac;
4843  core_anum_t ur;
4844  const char* fcs;
4845 
4846  // Show not more than 500 groups
4847  if((std::size_t) 500 >= i)
4848  {
4849  // Clamp article count to process
4850  groupCAC(&subscribedGroups[i]);
4851 
4852  // Calculate number of unread articles
4853  eac = subscribedGroups[i].eac;
4854  ur = groupGetUnreadNo(subscribedGroups[i].lwm, subscribedGroups[i].hwm,
4855  group_list[i].info);
4856  // Create entry for group list widget
4857  if(ur) { fcs = "@b"; } else { fcs = "@."; }
4858  groupString << fcs << group_list[i].name
4859  << " (" << ur << " / " << eac << ")" << std::flush;
4860 
4861  // Attention:
4862  //
4863  // const char* s = groupString.str().c_str()
4864  //
4865  // creates 's' with undefined value because the compiler is allowed to
4866  // free the temporary string object returned by 'str()' immediately after
4867  // the assignment!
4868  // Assigning names to temporary string objects forces them to stay in
4869  // memory as long as their names go out of scope (this is what we need).
4870  const std::string& gs = groupString.str();
4871 
4872  // Note: The index in the group list widget starts with 1
4873  if(++i > (std::size_t) groupList->size())
4874  {
4875  // Create new entry
4876  groupList->add(gs.c_str(), NULL);
4877  }
4878  else
4879  {
4880  // Replace label of existing entry
4881  groupList->text((int) i, gs.c_str());
4882  }
4883  }
4884 }
4885 
4886 
4887 // =============================================================================
4888 // Merge current group states into groupfile
4889 
4890 int MainWindow::groupStateMerge(void)
4891 {
4892  int res;
4893 
4894  // Skip this if group count is zero (or the groupfile will be cleared)
4895  if(group_num)
4896  {
4897  res = core_export_group_states(group_num, group_list);
4898  if(res)
4899  {
4900  SC("Do not use non-ASCII for the translation of this item")
4901  fl_message_title(S("Error"));
4902  fl_alert("%s", S("Exporting group states failed"));
4903  }
4904  }
4905  // Re-import current group states from groupfile
4906  res = core_update_subscribed_group_states(&group_num, &group_list,
4907  &group_list_index);
4908 
4909  return(res);
4910 }
4911 
4912 
4913 // =============================================================================
4914 // Main window ask server for proposed groups to subscribe
4915 // (must be locked)
4916 
4917 void MainWindow::groupListGetProposal(int action)
4918 {
4919  int rv = -1;
4920  char* grouplist_raw;
4921 
4922  // Start core to do the work
4923  if(UI_CB_START == action)
4924  {
4925  // Group list not empty
4926  if(!stateMachine(EVENT_GL_PROPOSAL)) { rv = 1; }
4927  else
4928  {
4929  // Get group states from server
4930  UI_STATUS(S("Get group subscription proposals ..."));
4931  UI_BUSY();
4932  rv = core_get_subscription_proposals(UI_CB_COOKIE_GROUPPROPOSAL);
4933  }
4934  }
4935  else
4936  {
4937  // Get result
4938  core_mutex_lock();
4939  rv = data.result;
4941  }
4942 
4943  // Check return value (positive value means "in progress")
4944  if(0 >= rv)
4945  {
4946  if(0 > rv)
4947  {
4948  // Operation failed
4949  UI_STATUS(S("Receiving proposed groups failed."));
4950  UI_READY();
4951  }
4952  else
4953  {
4954  core_mutex_lock();
4955  grouplist_raw = (char*) data.data;
4957  // Populate group list
4958  rv = gui_populate_group_list(grouplist_raw);
4959  if(rv)
4960  {
4961  PRINT_ERROR("Updating list of subscribed groups failed");
4962  }
4963  core_free(grouplist_raw);
4964  // Refresh group list
4965  groupListRefresh(UI_CB_START);
4966  }
4967  if(!stateMachine(EVENT_GL_PROPOSAL_EXIT))
4968  {
4969  PRINT_ERROR("Error in main window state machine");
4970  }
4971  }
4972 }
4973 
4974 
4975 // =============================================================================
4976 // Main window refresh group list with subscribed groups
4977 // (must be locked)
4978 
4979 void MainWindow::groupListRefresh(int action)
4980 {
4981  int rv = -1;
4982  std::size_t i;
4983 
4984  // Start core to do the work
4985  if(UI_CB_START == action)
4986  {
4987  // Group list not empty
4988  if(!stateMachine(EVENT_GL_REFRESH)) { rv = 1; }
4989  else
4990  {
4991  rv = groupStateMerge();
4992  if(!rv)
4993  {
4994  // Get group states from server
4995  UI_STATUS(S("Refreshing subscribed groups ..."));
4996  UI_BUSY();
4997  groupRefresh_cb_state = groupList->value();
4998  rv = core_get_subscribed_group_info(&group_num, &group_list,
4999  UI_CB_COOKIE_GROUPINFO1);
5000  // Note: This will generate callback with action UI_CB_CONTINUE
5001  }
5002  }
5003  }
5004  else
5005  {
5006  // Get result
5007  core_mutex_lock();
5008  rv = data.result;
5010  }
5011 
5012  if(!rv && UI_CB_CONTINUE == action)
5013  {
5014  // Continue operation
5015  if(NULL != subscribedGroups)
5016  {
5017  core_destroy_subscribed_group_info(&subscribedGroups);
5018  }
5019  core_mutex_lock();
5020  subscribedGroups = (core_groupdesc*) data.data;
5022  // Update group list widget
5023  groupList->clear();
5024  for(i = 0; i < group_num; ++i) { groupListUpdateEntry(i); }
5025  // Restore current group of server
5026  if(group_num && NULL != currentGroup)
5027  {
5028  rv = core_set_group(group_list[group_list_index].name,
5029  UI_CB_COOKIE_GROUPINFO2);
5030  // Note: This will generate callback with action UI_CB_FINISH
5031  }
5032  }
5033 
5034  if(!rv && UI_CB_FINISH == action)
5035  {
5036  // Update current group descriptor
5037  core_free(currentGroup);
5038  core_mutex_lock();
5039  currentGroup = (core_groupdesc*) data.data;
5041  }
5042 
5043  // Check return value (positive value means "in progress")
5044  if(0 >= rv)
5045  {
5046  if(0 > rv)
5047  {
5048  // Operation failed
5049  clearTree();
5050  core_free(currentGroup);
5051  currentGroup = NULL;
5052  UI_STATUS(S("Refreshing subscribed groups failed."));
5053  }
5054  else
5055  {
5056  if(!group_num || unsub)
5057  {
5058  // Delete article tree and current article
5059  clearTree();
5060  core_free(currentGroup);
5061  currentGroup = NULL;
5062  }
5063  else
5064  {
5065  // Reset group list widget
5066  groupList->value(groupRefresh_cb_state);
5067  }
5068  // Operation finished
5069  UI_STATUS(S("Subscribed groups refreshed."));
5070  }
5071  unsub = false;
5072  UI_READY();
5073  if(!stateMachine(EVENT_GL_REFRESH_EXIT))
5074  {
5075  PRINT_ERROR("Error in main window state machine");
5076  }
5077  }
5078 }
5079 
5080 
5081 // =============================================================================
5082 // Main window group selection
5083 // (must be locked)
5084 
5085 void MainWindow::groupSelect(int action, int index)
5086 {
5087  int rv = -1;
5088 
5089  if(UI_CB_START == action)
5090  {
5091  if(!index || !stateMachine(EVENT_G_SELECT))
5092  {
5093  if(NULL != currentGroup)
5094  {
5095  // Reset group list window if operation is not allowed
5096  if((std::size_t) INT_MAX > group_list_index)
5097  {
5098  groupList->value((int) group_list_index + 1);
5099  }
5100  }
5101  rv = 1;
5102  }
5103  else
5104  {
5105  // Start core to do the work
5106  if(0 < index)
5107  {
5108  group_new = index;
5109  if(group_old)
5110  {
5111  group_old = 0;
5112  if((std::size_t) INT_MAX > group_list_index)
5113  {
5114  group_old = (int) group_list_index + 1;
5115  }
5116  }
5117  group_list_index = (std::size_t) index;
5118  if(--group_list_index < group_num)
5119  {
5120  UI_STATUS(S("Set new current group ..."));
5121  UI_BUSY();
5122  groupSelect_cb_state = groupList->value();
5123  rv = core_set_group(group_list[group_list_index].name,
5124  UI_CB_COOKIE_GROUP);
5125  }
5126  }
5127  }
5128  }
5129  else
5130  {
5131  // Get result
5132  core_mutex_lock();
5133  rv = data.result;
5135  }
5136 
5137  // Check return value (positive value means "in progress")
5138  if(0 >= rv)
5139  {
5140  UI_READY();
5141  if(0 > rv)
5142  {
5143  clearTree();
5144  UI_STATUS(S("Setting new current group failed."));
5145  // After error, the previous group is still selected
5146  if(0 >= group_old)
5147  {
5148  // No previous group
5149  groupList->select(group_new, 0);
5150  }
5151  else
5152  {
5153  group_list_index = group_old - 1;
5154  groupList->value(group_old);
5155  }
5156  if(stateMachine(EVENT_AT_REFRESH))
5157  {
5158  if(!stateMachine(EVENT_AT_REFRESH_EXIT))
5159  {
5160  PRINT_ERROR("Error in main window state machine");
5161  }
5162  }
5163  }
5164  else
5165  {
5166  // Success, update current group
5167  group_old = -1;
5168  core_free(currentGroup);
5169  core_mutex_lock();
5170  currentGroup = (core_groupdesc*) data.data;
5172  // Clamp article count to process
5173  groupCAC(currentGroup);
5174  UI_STATUS(S("New current group set."));
5175 
5176  // Update article tree
5177  lastArticleHE = NULL;
5178  currentArticleHE = NULL;
5179  updateArticleTree(UI_CB_START);
5180  }
5181  if(!stateMachine(EVENT_G_SELECT_EXIT))
5182  {
5183  PRINT_ERROR("Error in main window state machine");
5184  }
5185  }
5186 }
5187 
5188 
5189 // =============================================================================
5190 // Main window clamp article count to configured value
5191 
5192 void MainWindow::groupCAC(core_groupdesc* g)
5193 {
5194  int i = config[CONF_CAC].val.i;
5195  core_anum_t diff;
5196 
5197  if(NULL != g && 0 < i--)
5198  {
5199  // Ensure that group is not empty
5200  if(g->hwm >= g->lwm && g->eac)
5201  {
5202  // Calculate new watermark difference
5203  diff = (core_anum_t) i;
5204  // Check whether clamping is required
5205  if(g->hwm - g->lwm > diff) { g->lwm = g->hwm - diff; }
5206  }
5207  }
5208 }
5209 
5210 
5211 // =============================================================================
5212 // Main window clear article tree (and current article)
5213 
5214 void MainWindow::clearTree(void)
5215 {
5216  Fl_Text_Buffer* tb;
5217  Fl_Tree_Item* ti;
5218 
5219  // Delete article tree
5220  articleTree->item_labelfont(FL_HELVETICA);
5221  articleTree->clear();
5222  ti = articleTree->add(S("No articles"));
5223  if(NULL != ti) { ti->deactivate(); }
5224  articleTree->redraw();
5225  // Hierarchy stays in memory until next group is selected
5226  // Delete last article
5227  lastArticleHE = NULL;
5228  // Delete current article
5229  currentArticleHE = NULL;
5230  // Preserve greeting message on startup
5231  if(startup) { startup = false; }
5232  else
5233  {
5234  tb = new Fl_Text_Buffer(0, 0);
5235  articleUpdate(tb);
5236  }
5237 }
5238 
5239 
5240 // =============================================================================
5241 // Main Window scroll article tree
5242 //
5243 // The original Fl_Tree widget of 1.3.0 requires the following algorithm for
5244 // scrolling to work correctly:
5245 // - Set vertical position to zero
5246 // - Redraw widget to update internal state
5247 // - Wait until redraw is complete
5248 // - Scroll to target position
5249 //
5250 // Since 1.3.3 the new Fl_Tree widget provides a 'calc_tree()' method.
5251 
5252 void MainWindow::scrollTree(ui_scroll position, Fl_Tree_Item* ti)
5253 {
5254  bool old_tree_widget = true;
5255 
5256  // Prepare internal state of widget
5257 #ifdef FL_ABI_VERSION
5258 # if 10303 <= FL_ABI_VERSION
5259  // Requires FLTK 1.3.3
5260  articleTree->hposition(0);
5261  articleTree->calc_tree();
5262  articleTree->hposition(0);
5263  old_tree_widget = false;
5264 # endif // 10303 <= FL_ABI_VERSION
5265 #endif // FL_ABI_VERSION
5266  if(old_tree_widget)
5267  {
5268  // Move vertical scrollbar to the top first
5269  articleTree->vposition(0);
5270  // Redraw tree so that new internal state is calculated for scrolling
5271  articleTree->redraw();
5272  articleTree->not_drawn();
5273  // Wait until redraw is finished
5274  while(!articleTree->drawn()) { Fl::wait(0.10); }
5275  // Wait until redraw is complete
5276  }
5277 
5278  // Scroll horizontally
5279 #ifdef FL_ABI_VERSION
5280 # if 10303 <= FL_ABI_VERSION
5281  // Requires FLTK 1.3.3
5282  gui_set_default_font();
5283  {
5284  int xmax = ti->draw_item_content(0);
5285  int scroll = xmax - (articleTree->x() + articleTree->w());
5286 
5287  // Check whether vertical scrollbar is visible
5288  if(articleTree->is_vscroll_visible())
5289  {
5290  // Yes => Add width of vertical scrollbar
5291  int w_sb = articleTree->scrollbar_size();
5292  if (0 == w_sb) { w_sb = Fl::scrollbar_size(); }
5293  scroll += w_sb;
5294  }
5295  if (0 < scroll)
5296  {
5297  // Check whether left side will be scrolled out of view
5298  int left = ti->x() - articleTree->x();
5299  if (left < scroll)
5300  {
5301  // Yes => Scroll to the left side
5302  scroll = left;
5303  }
5304  articleTree->hposition(scroll);
5305  }
5306  }
5307 # endif // 10303 <= FL_ABI_VERSION
5308 #endif // FL_ABI_VERSION
5309 
5310  // Scroll vertically
5311  switch(position)
5312  {
5313  case UI_SCROLL_TOP:
5314  {
5315  articleTree->show_item_top(ti);
5316  break;
5317  }
5318  case UI_SCROLL_MIDDLE:
5319  {
5320  articleTree->show_item_middle(ti);
5321  break;
5322  }
5323  case UI_SCROLL_BOTTOM:
5324  {
5325  articleTree->show_item_bottom(ti);
5326  break;
5327  }
5328  case UI_SCROLL_NONE:
5329  default:
5330  {
5331  // Ignore ti pointer
5332  break;
5333  }
5334  }
5335 
5336  articleTree->redraw();
5337 }
5338 
5339 
5340 // =============================================================================
5341 // Main Window check whether tree branch below item contains unread articles
5342 // This method must be reentrant.
5343 
5344 bool MainWindow::checkTreeBranchForUnread(Fl_Tree_Item* ti)
5345 {
5346  bool res = false;
5347  int c;
5348  int i;
5349 
5350  if(ti->children())
5351  {
5352  c = ti->children();
5353  for(i = 0; i < c; ++i)
5354  {
5355  // Check this child
5356  if(FL_HELVETICA_BOLD == ti->child(i)->labelfont()) { res = true; }
5357  else
5358  {
5359  // Recursively check children of this child
5360  if(checkTreeBranchForUnread(ti->child(i))) { res = true; }
5361  }
5362  if(true == res) { break; }
5363  }
5364  }
5365 
5366  return(res);
5367 }
5368 
5369 
5370 // =============================================================================
5371 // Main Window check whether tree branch below 'ti' contains item 'sti'
5372 // This method must be reentrant.
5373 
5374 bool MainWindow::checkTreeBranchForItem(Fl_Tree_Item* ti, Fl_Tree_Item* sti)
5375 {
5376  bool res = false;
5377  int c;
5378  int i;
5379 
5380  if(ti->children())
5381  {
5382  c = ti->children();
5383  for(i = 0; i < c; ++i)
5384  {
5385  // Check this child
5386  if(ti->child(i) == sti) { res = true; }
5387  else
5388  {
5389  // Recursively check children of this child
5390  if(checkTreeBranchForItem(ti->child(i), sti)) { res = true; }
5391  }
5392  if(true == res) { break; }
5393  }
5394  }
5395 
5396  return(res);
5397 }
5398 
5399 
5400 // =============================================================================
5401 // Main window add children to article tree node
5402 //
5403 // \param[in] cti Pointer to node of tree widget
5404 // \param[in] che Pointer to node of core article hierarchy
5405 //
5406 // Call this method with \e cti and \e che pointing to the root node of the
5407 // corresponding hierarchy. To add the child nodes, this method calls itself
5408 // recursively and must therefore be reentrant.
5409 //
5410 // This method first adds every node with bold font (marked unread).
5411 // Then the corresponding article is checked. If it's already read, the font is
5412 // changed to normal style (marked read).
5413 //
5414 // \note
5415 // This method is called for every article until CAC limit. It should be fast.
5416 //
5417 // \return
5418 // - The corresponding tree item if the current article was found
5419 // - \c NULL otherwise
5420 
5421 Fl_Tree_Item* MainWindow::addTreeNodes(Fl_Tree_Item* cti,
5423 {
5424  static const char t[] = " | "; // Field separator
5425  std::size_t t_len = sizeof(t);
5426  const char* s; // Subject
5427  std::size_t s_len;
5428  char s_tmp; // Temporary buffer used to convert HTAB to SP
5429  const char* f; // From
5430  std::size_t f_len;
5431  char* f_tmp; // Pointer to temporary buffer used for <angle-addr>
5432  core_time_t d_raw;
5433  char d[20]; // Date (Format: "YYYY-MM-DD HH:MM:SS")
5434  std::size_t d_len;
5435 #if USE_LINE_COUNT
5436  unsigned long int l_raw;
5437  char l[11]; // Lines
5438 #endif // USE_LINE_COUNT
5439 #if USE_ARTICLE_NUMBER
5440  char a[17]; // Article number
5441 #endif // USE_ARTICLE_NUMBER
5442  std::size_t l_len = 0;
5443  std::size_t a_len = 0;
5444  Fl_Tree_Item* res = NULL;
5445  Fl_Tree_Item* tmp = NULL;
5446  Fl_Tree_Item* nti = NULL;
5447  char* ss;
5448  std::size_t ssi;
5449  std::size_t i;
5450  std::size_t ii;
5451  int rv;
5452  int score;
5453  // For merging orphaned thread branches
5454  bool child_of_root_node = false;
5455  Fl_Tree_Item* rti;
5456  int iii;
5457  struct core_article_header* h;
5458  struct core_article_header* h2;
5459  // For unthreaded view
5460  core_time_t rd;
5461  core_anum_t n;
5462  core_anum_t rn;
5463  bool match;
5464 
5465  // Explicitly set foreground color for tree items to FLTK default
5466  // (default is not the same as for the other widgets)
5467  articleTree->item_labelfgcolor(FL_FOREGROUND_COLOR);
5468 
5469  // Set default font to bold (mark unread)
5470  articleTree->item_labelfont(FL_HELVETICA_BOLD);
5471 
5472  // Add children of current hierarchy element
5473  if(articleTree->root() == cti) { child_of_root_node = true; }
5474  for(i = 0; i < che->children; ++i)
5475  {
5476  n = che->child[i]->anum;
5477  // Create description line in format "Subject | From | Date[ | Lines]"
5478  // Prepare data and calculate length
5479  s = che->child[i]->header->subject;
5480  s_len = std::strlen(s);
5481  f = che->child[i]->header->from;
5482  f_len = t_len + std::strlen(f);
5483  d_raw = che->child[i]->header->date;
5484  rv = enc_convert_posix_to_iso8601(d, d_raw);
5485  if(rv) { d_len = 0; } else { d_len = t_len + (std::size_t) 20; }
5486 #if USE_LINE_COUNT
5487  l_raw = che->child[i]->header->lines;
5488  enc_convert_lines_to_string(l, l_raw);
5489  l_len = t_len + (std::size_t) 11;
5490 #else // USE_LINE_COUNT
5491  l_len = 0;
5492 #endif // USE_LINE_COUNT
5493 #if USE_ARTICLE_NUMBER
5494  rv = enc_convert_anum_to_ascii(a, &a_len, che->child[i]->anum);
5495  if(rv)
5496  {
5497  a[0] = 'E';
5498  a[1] = 'r';
5499  a[2] = 'r';
5500  a[3] = 'o';
5501  a[4] = 'r';
5502  a[5] = 0;
5503  a_len = 0;
5504  }
5505  else { a_len += t_len; }
5506 #else // USE_ARTICLE_NUMBER
5507  a_len = 0;
5508 #endif // USE_ARTICLE_NUMBER
5509  // Allocate buffer for description line
5510  ss = new char[s_len + f_len + d_len + l_len + a_len + (std::size_t) 1];
5511  ssi = 0; // Current index in 'ss'
5512  // -----------------------------------------------------------------------
5513  // Subject
5514  ii = 0; while(s[ii])
5515  {
5516  s_tmp = s[ii++];
5517  // The NNTP command OVER cannot transport HTAB characters (converts
5518  // them to SP according to RFC 3977). We always do this here to get
5519  // consistent results for cases where OVER is not used or the HTAB was
5520  // transported inside a MIME encoded-word.
5521  if (0x09 == (int) s_tmp) { ss[ssi++] = ' '; }
5522  else { ss[ssi++] = s_tmp; }
5523  }
5524  // -----------------------------------------------------------------------
5525  // From
5526  ii = 0; while(t[ii]) { ss[ssi++] = t[ii++]; }
5527  // Strip 'angle-addr' with standard method
5528  // (Note: Optimization was tested to be useless here)
5529  f_tmp = new char[f_len + (std::size_t) 1]; // For zero length separator
5530  std::strcpy(f_tmp, f);
5531  stripAngleAddress(f_tmp);
5532  ii = 0; while(f_tmp[ii]) { ss[ssi++] = f_tmp[ii++]; }
5533  delete[] f_tmp;
5534  // -----------------------------------------------------------------------
5535  // Date
5536  if(!rv)
5537  {
5538  ii = 0; while(t[ii]) { ss[ssi++] = t[ii++]; }
5539  ii = 0; while(d[ii]) { ss[ssi++] = d[ii++]; }
5540  }
5541  // -----------------------------------------------------------------------
5542  // Line count
5543 #if USE_LINE_COUNT
5544  ii = 0; while(t[ii]) { ss[ssi++] = t[ii++]; }
5545  ii = 0; while(l[ii]) { ss[ssi++] = l[ii++]; }
5546 #endif // USE_LINE_COUNT
5547  // -----------------------------------------------------------------------
5548  // Article number
5549 #if USE_ARTICLE_NUMBER
5550  ii = 0; while(t[ii]) { ss[ssi++] = t[ii++]; }
5551  ii = 0; while(a[ii]) { ss[ssi++] = a[ii++]; }
5552 #endif // USE_ARTICLE_NUMBER
5553  // -----------------------------------------------------------------------
5554  // Terminate description line string
5555  ss[ssi] = 0;
5556 
5557  // Add new node with the created description line
5558  if(config[CONF_TVIEW].val.i)
5559  {
5560  // --------------------------------------------------------------------
5561  // Insert new node (threaded view)
5562  if(child_of_root_node)
5563  {
5564  // Special handling for children of the root node
5565  rti = NULL;
5566  for(iii = 0; iii < cti->children(); ++iii)
5567  {
5568  // Try to merge orphaned thread branches by first reference
5569  // (all articles with this same anchor belong to the same thread)
5570  h = che->child[i]->header;
5571  if(NULL != h->refs)
5572  {
5573  h2 = ((core_hierarchy_element*) cti->child(iii)->user_data())
5574  ->header;
5575  if(NULL != h2->refs)
5576  {
5577  if(!strcmp(h2->refs[0], h->refs[0]))
5578  {
5579  rti = cti->child(iii);
5580  break;
5581  }
5582  }
5583  }
5584  }
5585  if(NULL == rti) { rti = cti; }
5586  nti = articleTree->add(rti, ss);
5587  }
5588  else { nti = articleTree->add(cti, ss); }
5589  // --------------------------------------------------------------------
5590  }
5591  else
5592  {
5593  // --------------------------------------------------------------------
5594  // Insert new node as child of root node (unthreaded view)
5595  for(rti = articleTree->next(articleTree->first());
5596  rti; rti = articleTree->next(rti))
5597  {
5598  match = false;
5599  if(config[CONF_UTVIEW_AN].val.i)
5600  {
5601  // Sort unthreaded view by article number
5602  rn = ((core_hierarchy_element*) rti->user_data())->anum;
5603  if(!config[CONF_INV_ORDER].val.i)
5604  {
5605  // Normal order (older above newer)
5606  if(rn >= n) { match = true; }
5607  }
5608  else
5609  {
5610  // Inversed order (older below newer)
5611  if(rn < n) { match = true; }
5612  }
5613  }
5614  else
5615  {
5616  // Sort unthreaded view by posting data
5617  rd = ((core_hierarchy_element*) rti->user_data())->header->date;
5618  if(!config[CONF_INV_ORDER].val.i)
5619  {
5620  // Normal order (older above newer)
5621  if(rd >= d_raw) { match = true; }
5622  }
5623  else
5624  {
5625  // Inversed order (older below newer)
5626  if(rd < d_raw) { match = true; }
5627  }
5628  }
5629  if(match)
5630  {
5631  nti = articleTree->insert_above(rti, ss);
5632  break;
5633  }
5634  }
5635  if(NULL == rti) { nti = articleTree->add(articleTree->root(), ss); }
5636  // --------------------------------------------------------------------
5637  }
5638  nti->user_data((void*) che->child[i]);
5639  delete[] ss;
5640 
5641  // -----------------------------------------------------------------------
5642  // Check whether node gets an icon
5643  // Priority order (low to high):
5644  // Positive score -> Reply to own -> Negative score -> Own
5645  score = filter_get_score(che->child[i]);
5646  // Check for positive score
5647  if(0 < score) { nti->usericon(&pm_score_up); }
5648  // Check for reply to own article
5649  if(filter_match_reply_to_own(che->child[i]))
5650  {
5651  nti->usericon(&pm_reply_to_own);
5652  }
5653  // Check for negative score
5654  if(0 > score)
5655  {
5656  nti->usericon(&pm_score_down);
5657  // Mark article read
5658  core_mark_as_read(&group_list[group_list_index], che->child[i]->anum);
5659  }
5660  // Check for own article
5661  if(filter_match_own(che->child[i])) { nti->usericon(&pm_own); }
5662  // -----------------------------------------------------------------------
5663 
5664  // Check whether article that corresponds to this node was already read
5665  if(core_check_already_read(&group_list[group_list_index], che->child[i]))
5666  {
5667  nti->labelfont(FL_HELVETICA);
5668  }
5669 
5670  // Recursively add children of current node
5671  if(che->child[i]->children) { tmp = addTreeNodes(nti, che->child[i]); }
5672  // Check whether article was the last one read
5673  if(NULL == res)
5674  {
5675  if(NULL != tmp) { res = tmp; }
5676  else
5677  {
5678  if(group_list[group_list_index].last_viewed == che->child[i]->anum)
5679  {
5680  res = nti;
5681  }
5682  }
5683  }
5684  }
5685 
5686  // Update group list
5687  groupListUpdateEntry(group_list_index);
5688 
5689  return(res);
5690 }
5691 
5692 
5693 // =============================================================================
5694 // Main window update article tree widget
5695 
5696 void MainWindow::updateTree(void)
5697 {
5698  int rv;
5700  Fl_Tree_Item* rti; // Root tree item
5701  Fl_Tree_Item* child_of_root;
5702  Fl_Tree_Item* sti = NULL; // Stored (last viewed) tree item
5703  int i;
5704  bool unread_articles_present;
5705  Fl_Tree_Item* ti;
5706  Fl_Tree_Item* oi;
5707  bool recalculate = true;
5708  bool abort = false;
5709 
5710  articleTree->update_in_progress(true);
5711 
5712  // Redraw tree so that new internal state is recalculated
5713  articleTree->redraw();
5714  articleTree->not_drawn();
5715  // Wait until redraw is finished
5716  while(!articleTree->drawn()) { Fl::wait(0.10); }
5717 
5718  // Create a root item if there is none
5719  rti = articleTree->root();
5720  if(!rti)
5721  {
5722  articleTree->add("X");
5723  rti = articleTree->root();
5724  }
5725  // Delete all children of root item
5726  articleTree->clear_children(rti);
5727 
5728  // Redraw tree so that new internal state is recalculated
5729  articleTree->redraw();
5730  articleTree->not_drawn();
5731  // Wait until redraw is finished
5732  while(!articleTree->drawn()) { Fl::wait(0.10); }
5733 
5734  // Load overview lines from core article hierarchy into the tree widget
5735  rv = core_hierarchy_manager(NULL, CORE_HIERARCHY_GETROOT, 0, &che);
5736  if(!rv) { sti = addTreeNodes(rti, che); }
5737  // Collapse all branches of tree
5738  collapseTree();
5739 
5740  // Delete all branches that contain no unread articles on request
5741  if(config[CONF_ONLYUR].val.i)
5742  {
5743  while(recalculate)
5744  {
5745  recalculate = false;
5746  rv = rti->children();
5747  for(i = 0; i < rv; ++i)
5748  {
5749  if(FL_HELVETICA_BOLD == rti->child(i)->labelfont())
5750  {
5751  unread_articles_present = true;
5752  }
5753  else
5754  {
5755  unread_articles_present
5756  = checkTreeBranchForUnread(rti->child(i));
5757  }
5758  if(!unread_articles_present)
5759  {
5760  // Check whether last viewed article will be deleted
5761  if( rti->child(i) == sti
5762  || checkTreeBranchForItem(rti->child(i), sti) )
5763  {
5764  sti = NULL;
5765  }
5766  // This method deletes all children too
5767  articleTree->remove(rti->child(i));
5768  recalculate = true;
5769  break;
5770  }
5771  }
5772  }
5773  // Check for empty tree
5774  if(!rti->children())
5775  {
5776  ti = articleTree->add(S("No articles"));
5777  if(NULL != ti)
5778  {
5779  ti->deactivate();
5780  ti->labelfont(FL_HELVETICA);
5781  ti->user_data(NULL);
5782  }
5783  }
5784  }
5785 
5786  // Show the toplevel item of each branch with bold font to indicate that it
5787  // contains unread articles
5788  rv = rti->children();
5789  for(i = 0; i < rv; ++i)
5790  {
5791  unread_articles_present = checkTreeBranchForUnread(rti->child(i));
5792  if(unread_articles_present)
5793  {
5794  rti->child(i)->labelfont(FL_HELVETICA_BOLD);
5795  }
5796  }
5797 
5798  // Check whether there is a last viewed article stored
5799  if(NULL != sti)
5800  {
5801  // Select last viewed article
5802  ti = rti;
5803  // Assignment in truth expression is intended
5804  while(NULL != (ti = articleTree->next(ti)))
5805  {
5806  if(sti == ti)
5807  {
5808  // Open branch with last viewed article
5809  oi = ti;
5810  while(articleTree->root() != oi)
5811  {
5812  if(!articleTree->open(oi)) { articleTree->open(oi, 1); }
5813  for(i = 0; i < articleTree->root()->children(); ++i)
5814  {
5815  if(articleTree->root()->child(i) == oi)
5816  {
5817  abort = true;
5818  break;
5819  }
5820  }
5821  if(abort) { break; }
5822  oi = articleTree->prev(oi);
5823  }
5824  // Select last viewed article
5825  articleTree->set_item_focus(ti);
5826  articleTree->select(ti, 1);
5827  Fl::focus(articleTree);
5828  break;
5829  }
5830  }
5831  }
5832  else
5833  {
5834  // Scroll down to last (or first) branch
5835  if(rti->children())
5836  {
5837  if(!config[CONF_INV_ORDER].val.i)
5838  {
5839  // Last branch
5840  child_of_root = rti->child(rti->children() - 1);
5841  }
5842  else
5843  {
5844  // First branch (inverted order)
5845  child_of_root = rti->child(0);
5846  }
5847  scrollTree(UI_SCROLL_BOTTOM, child_of_root);
5848  articleTree->set_item_focus(child_of_root);
5849  Fl::focus(articleTree);
5850  }
5851  // Clear article content window
5852  currentArticle->text("");
5853  currentStyle->text("");
5854  }
5855  articleTree->redraw();
5856 
5857  articleTree->update_in_progress(false);
5858 }
5859 
5860 
5861 // =============================================================================
5862 // Main window create article hierarchy
5863 // (must be locked)
5864 
5865 void MainWindow::updateArticleTree(int action)
5866 {
5867  int rv = -1; // Negative: Error, 0: Success, 1: In progress, 2: Empty
5868  const char* header;
5869 
5870  if(UI_CB_START == action)
5871  {
5872  if(!stateMachine(EVENT_AT_REFRESH)) { rv = 1; }
5873  else
5874  {
5875  state = 0;
5876  // Push horizontal scrollbar left
5877 #ifdef FL_ABI_VERSION
5878 # if 10303 <= FL_ABI_VERSION
5879  // Requires FLTK 1.3.3
5880  articleTree->hposition(0);
5881 # endif // 10303 <= FL_ABI_VERSION
5882 #endif // FL_ABI_VERSION
5883  // Init article hierarchy
5885  if(!rv)
5886  {
5887  // Check whether group is empty
5888  if(!currentGroup->eac) { rv = 2; }
5889  else
5890  {
5891  // No => Fetch 1st header
5892  ai = currentGroup->lwm;
5893  UI_STATUS(S("Update article tree ..."));
5894  UI_BUSY();
5895  // Try to fetch header overview
5896  ai_range.first = currentGroup->lwm;
5897  ai_range.last = currentGroup->hwm;
5898  ai_range.next = NULL;
5899  rv = core_get_overview(&ai_range, UI_CB_COOKIE_OVERVIEW);
5900  if(0 > rv)
5901  {
5902  // No header overview available => Fetch 1st header
5903  rv = core_get_article_header(&ai, UI_CB_COOKIE_HEADER);
5904  }
5905  }
5906  }
5907  }
5908  }
5909  else
5910  {
5911  if(-1 != state)
5912  {
5913  // Get result
5914  core_mutex_lock();
5915  rv = data.result;
5917  }
5918  }
5919 
5920  // Check return value
5921  if(0 >= rv || 2 == rv)
5922  {
5923  if(0 > rv)
5924  {
5925  // Failed
5926  clearTree();
5927  state = -1;
5928  }
5929  else if(2 == rv)
5930  {
5931  // Empty
5932  clearTree();
5933  state = 2;
5934  }
5935  else
5936  {
5937  // Success
5938  core_mutex_lock();
5939  header = (const char*) data.data;
5941  // Check whether overview is available
5942  if(UI_CB_FINISH == action)
5943  {
5944  core_create_hierarchy_from_overview(&group_list[group_list_index],
5945  &ai_range, header);
5946  core_free((void*) header);
5947  // Finished
5948  state = 1;
5949  }
5950  else
5951  {
5952  UI_PROGRESS((std::size_t) ai - currentGroup->lwm,
5953  (std::size_t) currentGroup->hwm - currentGroup->lwm);
5954  // Check for last article
5955  if(currentGroup->hwm == ai++)
5956  {
5957  // Finished
5958  state = 1;
5959  }
5960  else
5961  {
5962  // Fetch next header in parallel using core thread
5963  rv = core_get_article_header(&ai, UI_CB_COOKIE_HEADER);
5964  if(0 > rv) { state = -1; }
5965  }
5966  // Check whether article was canceled
5967  if(NULL == header)
5968  {
5969  // Yes => Mark read to achieve correct number of unread articles
5970  core_mark_as_read(&group_list[group_list_index], ai - 1U);
5971  }
5972  else
5973  {
5974  // No => Insert article into hierarchy
5976  ai - 1U, header);
5977  if(0 > rv)
5978  {
5979  PRINT_ERROR("Adding article to hierarchy failed");
5980  // Note: Don't change state here and continue
5981  }
5982  core_free((void*) header);
5983  }
5984  }
5985  }
5986  // Note: 'state' stay zero if there are more articles to fetch
5987  if(state)
5988  {
5989  // Reset group list widget
5990  groupList->value(groupSelect_cb_state);
5991  // Unlock before updating article tree widget
5992  if(!stateMachine(EVENT_AT_REFRESH_EXIT))
5993  {
5994  PRINT_ERROR("Error in main window state machine");
5995  }
5996  switch(state)
5997  {
5998  case 2: // Empty
5999  {
6000  UI_STATUS(S("Group is empty"));
6001  break;
6002  }
6003  case 1: // Finished
6004  {
6005  // Update UI widgets with data from core
6006  UI_BUSY();
6007  Fl::check();
6008  updateTree();
6009  UI_READY();
6010  UI_STATUS(S("Article tree updated."));
6011  break;
6012  }
6013  case -1: // Aborted
6014  {
6015  UI_READY();
6016  UI_STATUS(S("Updating article tree failed."));
6017  break;
6018  }
6019  default: // Bug
6020  {
6021  PRINT_ERROR("Invalid state while updating article tree");
6022  UI_READY();
6023  UI_STATUS(S("Updating article tree failed."));
6024  break;
6025  }
6026  }
6027  }
6028  }
6029 }
6030 
6031 
6032 // =============================================================================
6033 // Main window article update
6034 
6035 void MainWindow::articleUpdate(Fl_Text_Buffer* article)
6036 {
6037  static bool init = true;
6038  // See RFC 3986 for URI format
6039  const char* url[] = { "http://", "https://", "ftp://", "nntp://",
6040  "file://", "news:", "mailto:", NULL };
6041  const std::size_t url_len[] = { 7, 8, 6, 7, 7, 5, 7 };
6042  const char bold = 'A'; // Bold text for header field names
6043  const char sig = 'B'; // Signature starting with "-- " separator
6044  const char cit = 'C'; // External citation ("|" at start of line)
6045  const char link = 'D'; // Hyperlink
6046  const char plain = 'E'; // Normal article text
6047  const char l1 = 'F'; // 1st citation level
6048  const char l2 = 'G'; // 2nd citation level
6049  const char l3 = 'H'; // 3rd citation level
6050  const char l4 = 'I'; // 4th citation level
6051  Fl_Text_Buffer* ca = currentArticle;
6052  char* style;
6053  std::size_t len;
6054  std::size_t i;
6055  std::size_t ii = 0; // Index in current line
6056  std::size_t iii = 0;
6057  std::size_t iiii;
6058  std::size_t url_i;
6059  bool sol = true; // Start Of Line flag
6060  char hs = plain;
6061  bool references = false; // Flag indicating reference list in header
6062  bool ready = false; // Flag indicating positions beyond header separator
6063  int ss = 0;
6064  bool delim = false; // Hyperlink delimiter
6065  bool signature = false; // Flag indicating signature
6066  bool citation = false; // Flag indicating external citation
6067  bool hyperlink = false; // Flag indicating hyperlink
6068  std::size_t cl = 0;
6069  bool cl_lock = false;
6070  char c;
6071  int pos = 0;
6072 
6073  // Store hyperlink style
6074  hyperlinkStyle = (unsigned int) (unsigned char) link;
6075 
6076  // Replace current article
6077  if(NULL == article)
6078  {
6079  PRINT_ERROR("Article update request without content ignored (bug)");
6080  return;
6081  }
6082  gui_check_article(article);
6083  currentArticle = article;
6084  text->buffer(currentArticle);
6085  currentLine = 0;
6086  if(ca) { delete ca; }
6087 
6088  // Soft Hyphen (SHY) handling
6089  gui_process_shy(currentArticle);
6090 
6091  // Create article content style
6092  if(currentStyle) { delete currentStyle; }
6093  style = currentArticle->text();
6094  if(NULL == style) { len = 0; }
6095  else { len = std::strlen(style); }
6096  if(INT_MAX < len) { len = INT_MAX; }
6097  for(i = 0; i < len; ++i)
6098  {
6099  if('\n' == style[i])
6100  {
6101  sol = true;
6102  references = false;
6103  hyperlink = false;
6104  citation = false;
6105  continue;
6106  }
6107  if(hyperlink)
6108  {
6109  // Check for end of hyperlink
6110  // According to RFC 3986 whitespace, double quotes and angle brackets
6111  // are accepted as delimiters.
6112  // Whitespace is interpreted as SP or HT, Unicode whitespace is not
6113  // accepted as delimiter.
6114  c = style[i];
6115  if(' ' == c || 0x09 == (int) c || '>' == c || '"' == c)
6116  {
6117  hyperlink = false;
6118  }
6119  }
6120  // Highlight external citations (via '|' or '!')
6121  if(sol && ('|' == style[i] || '!' == style[i]))
6122  {
6123  if(!hyperlink) { citation = true; }
6124  }
6125  // Check for start of line
6126  if(sol) { sol = false; delim = true; ss = 0; cl = 0; ii = 0; }
6127  else { ++ii; }
6128  if(signature)
6129  {
6130  // Check for end of signature in potential multipart message
6131  if('|' == style[i])
6132  {
6133  if(79U == ii)
6134  {
6135  for(iiii = i - ii; iiii < i; ++iiii)
6136  {
6137  if('_' != currentArticle->byte_at((int) iiii)) { break; }
6138  }
6139  if(iiii == i)
6140  {
6141  for(iiii = 0; iiii <= ii; ++iiii)
6142  {
6143  style[i - iiii] = plain;
6144  }
6145  signature = false;
6146  }
6147  }
6148  }
6149  }
6150  if(!ready)
6151  {
6152  // Check for header separator <SOL>"____...____|"
6153  if(!ii)
6154  {
6155  if('_' != style[i]) { hs = bold; } else { hs = plain; }
6156  iii = 0;
6157  }
6158  if('|' == style[i] && 79U <= iii) { ready = true; }
6159  else if('_' == style[i]) { ++iii; }
6160  if(':' == style[i]) { hs = plain; }
6161  // Check for GS control character (reference link list marker)
6162  if(0x1D == (int) style[i]) { references = true; }
6163  if(references)
6164  {
6165  // Create hyperlinks to articles in references list
6166  if(0x30 <= style[i] && 0x39 >= style[i]) { hs = link; }
6167  else { hs = plain; }
6168  }
6169  style[i] = hs;
6170  }
6171  else
6172  {
6173  // Check for signature separator <SOL>"-- "
6174  if('-' == style[i] && !ii) { ss = 1; }
6175  if('-' == style[i] && 1U == ii && 1 == ss) { ss = 2; }
6176  if(' ' == style[i] && 2U == ii && 2 == ss)
6177  {
6178  // Attention: This EOL check requires POSIX line format!
6179  if((char) 0x0A == style[i + 1U])
6180  {
6181  if(gui_last_sig_separator(&style[i + 1U]))
6182  {
6183  style[i] = sig; style[i - 1U] = sig; style[i - 2U] = sig;
6184  signature = true;
6185  }
6186  }
6187  }
6188  // Check for hyperlink
6189  url_i = 0;
6190  while(NULL != url[url_i])
6191  {
6192  if(!std::strncmp(&style[i], url[url_i], url_len[url_i]))
6193  {
6194  if(delim)
6195  {
6196  style[i] = link;
6197  hyperlink = true;
6198  }
6199  }
6200  ++url_i;
6201  }
6202  c = style[i];
6203  if(' ' == c || 0x09 == (int) c || '<' == c || '"' == c)
6204  {
6205  delim = true;
6206  }
6207  else { delim = false; }
6208  if(1)
6209  {
6210  // Highlight citation levels of regular content
6211  if(!ii)
6212  {
6213  if('>' == style[i]) { cl_lock = false; }
6214  else { cl_lock = true; }
6215  }
6216  if('>' == style[i] && !cl_lock) { ++cl; }
6217  if('>' != style[i] && ' ' != style[i] && (char) 9 != style[i])
6218  {
6219  cl_lock = true;
6220  }
6221  if(4U < cl) { cl = 1; } // Rotate colors if too many levels
6222  switch(cl)
6223  {
6224  case 1: { style[i] = l1; break; }
6225  case 2: { style[i] = l2; break; }
6226  case 3: { style[i] = l3; break; }
6227  case 4: { style[i] = l4; break; }
6228  default: { style[i] = plain; break; }
6229  }
6230  }
6231  // Override current style for signature, citations and hyperlinks
6232  // (hyperlinks have highest precedence)
6233  if(signature) { style[i] = sig; }
6234  if(citation && !signature) { style[i] = cit; }
6235  if(hyperlink) { style[i] = link; }
6236  }
6237  }
6238  currentStyle = new Fl_Text_Buffer((int) len, 0);
6239  if(NULL != style) { currentStyle->text(style); }
6240  std::free((void*) style);
6241 
6242  // Special handling for greeting message (always display as plain text)
6243  if(init) { init = false; }
6244  else
6245  {
6246  // Activate content style
6247  text->highlight_data(currentStyle, styles, styles_len, 'A', NULL, NULL);
6248  }
6249 
6250  // Replace GS marker with SP
6251  if (1 == currentArticle->findchar_forward(0, 0x1DU, &pos))
6252  {
6253  currentArticle->replace(pos, pos + 1, " ");
6254  }
6255 }
6256 
6257 
6258 // =============================================================================
6259 // Main window format and print some header fields of article
6260 
6261 const char* MainWindow::printHeaderFields(struct core_article_header* h)
6262 {
6263  return(gui_print_header_fields(h));
6264 }
6265 
6266 
6267 // =============================================================================
6268 // Main window article selection
6269 // (must be locked)
6270 
6271 void MainWindow::articleSelect(int action)
6272 {
6273  int rv = -1;
6274  char* raw;
6275  char* eoh;
6276  const char* p = NULL;
6277  const char* q = NULL;
6278  Fl_Text_Buffer* tb;
6279  const char* hdr;
6280  bool overview = false;
6281  bool process = false;
6282 
6283  // Check whether current HE is available
6284  if(NULL == currentArticleHE)
6285  {
6286  // Print error message and terminate gracefully
6287  SC("Do not use non-ASCII for the translation of this item")
6288  fl_message_title(S("Error"));
6289  fl_alert("%s", "Fatal error detected in 'articleSelect()' (bug)");
6290  exitRequest = 1;
6291  return;
6292  }
6293 
6294  // Check whether current HE contains only overview data
6295  if(CORE_HE_FLAG_OVER & currentArticleHE->flags) { overview = true; };
6296 
6297  if(UI_CB_START == action)
6298  {
6299  // Check whether operation is allowed at the moment
6300  if(!stateMachine(EVENT_A_SELECT)) { rv = 1; }
6301  else if(currentArticleHE)
6302  {
6303  // Start core to fetch article body
6304  if(main_debug) { PRINT_ERROR("Article selected"); }
6305  UI_STATUS(S("Download article ..."));
6306  UI_BUSY();
6307  startup = false;
6308  if(overview)
6309  {
6310  // Fetch whole article (post-fetch complete header data)
6311  rv = core_get_article(&currentArticleHE->anum, UI_CB_COOKIE_BODY);
6312  }
6313  else
6314  {
6315  // Fetch only body, header data in HE are already complete
6316  rv = core_get_article_body(&currentArticleHE->anum,
6317  UI_CB_COOKIE_BODY);
6318  }
6319  }
6320  }
6321  else
6322  {
6323  // Get result
6324  core_mutex_lock();
6325  rv = data.result;
6327  }
6328 
6329  // Check return value (positive value means "in progress")
6330  if(0 >= rv)
6331  {
6332  UI_READY();
6333  if(!rv)
6334  {
6335  process = true;
6336  core_mutex_lock();
6337  raw = (char*) data.data;
6339  p = raw;
6340  // Reset wrap mode
6341  wrapMode = Fl_Text_Display::WRAP_NONE;
6342  text->wrap_mode(wrapMode, 0);
6343  // Extract header data and update HE if required
6344  if(!overview) { q = p; }
6345  else
6346  {
6347  // Search for end of header
6348  // Attention: Overloaded and both prototypes different than in C!
6349  eoh = std::strstr(raw, "\r\n\r\n");
6350  if(NULL == eoh)
6351  {
6352  core_free((void*) p);
6353  process = false;
6354  }
6355  else
6356  {
6357  // Set pointer to body data
6358  q = &eoh[4];
6359  // Update HE
6360  eoh[2] = 0;
6362  currentArticleHE->anum, p);
6363  if(0 > rv) { process = false; }
6364  }
6365  }
6366  }
6367  if(!process)
6368  {
6369  // Failed
6370  UI_STATUS(S("Download of article failed."));
6371  PRINT_ERROR("Processing of article failed");
6372  // Clear article content window
6373  currentArticle->text("");
6374  currentStyle->text("");
6375  }
6376  else
6377  {
6378  // Success, update article window
6379  UI_STATUS(S("Article successfully downloaded."));
6380  // Create text buffer
6381  tb = new Fl_Text_Buffer(0, 0);
6382 #if 0
6383  // For debugging
6384  // The 'printf()' implementation must be able to handle NULL pointers!
6385  std::printf("\nArticle watermark ............: %lu\n",
6386  currentArticleHE->anum);
6387  std::printf("MIME version .................: %s\n",
6388  currentArticleHE->header->mime_v);
6389  std::printf("MIME content transfer encoding: %s\n",
6390  currentArticleHE->header->mime_cte);
6391  std::printf("MIME content type ............: %s\n",
6392  currentArticleHE->header->mime_ct);
6393 #endif
6394  // Print formatted header data to text buffer
6395  hdr = printHeaderFields(currentArticleHE->header);
6396  if(NULL != hdr)
6397  {
6398  tb->text(hdr);
6399  delete[] hdr;
6400  }
6401  // Print header/body delimiter
6402  tb->append(ENC_DELIMITER);
6403  // Create MIME object from content
6404  if(NULL != mimeData) { delete mimeData; }
6405  mimeData = new MIMEContent(currentArticleHE->header, q);
6406  core_free((void*) p); p = NULL;
6407  gui_decode_mime_entities(tb, mimeData,
6408  currentArticleHE->header->msgid);
6409  // Print content
6410  articleUpdate(tb);
6411  // Store last viewed article
6412  group_list[group_list_index].last_viewed = currentArticleHE->anum;
6413  }
6414  // Mark current article read
6415  core_mark_as_read(&group_list[group_list_index],
6416  currentArticleHE->anum);
6417  groupListUpdateEntry(group_list_index);
6418  // Check for error
6419  if(!process) { currentArticleHE = NULL; }
6420  else
6421  {
6422  // Reset search start position
6423  currentSearchPosition = 0;
6424  }
6425  if(!stateMachine(EVENT_A_SELECT_EXIT))
6426  {
6427  PRINT_ERROR("Error in main window state machine");
6428  }
6429  }
6430 }
6431 
6432 
6433 // =============================================================================
6434 // Main window view message of the day callback
6435 // (must be locked)
6436 
6437 void MainWindow::viewMotd(int action)
6438 {
6439  int rv = -1;
6440  std::ostringstream titleString;
6441  const char* motd = NULL;
6442  const char* p;
6443 
6444  if(UI_CB_START == action)
6445  {
6446  if(!stateMachine(EVENT_MOTD_VIEW)) { rv = 1; }
6447  else
6448  {
6449  // Start core to do the work
6450  UI_STATUS(S("Get message of the day ..."));
6451  UI_BUSY();
6452  rv = core_get_motd(UI_CB_COOKIE_MOTD);
6453  }
6454  }
6455  else
6456  {
6457  // Get result
6458  core_mutex_lock();
6459  rv = data.result;
6461  }
6462 
6463  // Check return value (positive value means "in progress")
6464  if(0 >= rv)
6465  {
6466  UI_READY();
6467  if(0 > rv) { UI_STATUS(S("Downloading message of the day failed.")); }
6468  else
6469  {
6470  // Success
6471  core_mutex_lock();
6472  p = (const char*) data.data;
6474  UI_STATUS(S("Message of the day successfully downloaded."));
6475 
6476  // Verify UTF-8 encoding and convert to POSIX form
6477  if(enc_uc_check_utf8(p))
6478  {
6479  motd = core_convert_canonical_to_posix("[Invalid encoding]\r\n",
6480  1, 0);
6481  }
6482  else { motd = core_convert_canonical_to_posix(p, 1, 0); }
6483 
6484  // Create Unicode title for window
6485  SC("")
6486  SC("Do not use characters for the translation that cannot be")
6487  SC("converted to the ISO 8859-1 character set for this item.")
6488  SC("Leave the original string in place if in doubt.")
6489  titleString << S("Message of the day") << std::flush;
6490  SC("")
6491 
6492  // Attention:
6493  //
6494  // const char* title = titleString.str().c_str()
6495  //
6496  // creates 'title' with undefined value because the compiler is allowed
6497  // to free the temporary string object returned by 'str()' immediately
6498  // after the assignment!
6499  // Assigning names to temporary string objects forces them to stay in
6500  // memory as long as their names go out of scope (this is what we
6501  // need).
6502  const std::string& ts = titleString.str();
6503 
6504  // Set "Article source code" window title
6505 #if CFG_USE_XSI && !CFG_NLS_DISABLE
6506  // Convert window title to the encoding required by the window manager
6507  // (WM)
6508  // It is assumed that the WM can display either ISO 8859-1 or UTF-8
6509  // encoded window titles.
6510  const char* title;
6511  title = gui_utf8_iso(ts.c_str());
6512  if(NULL != title)
6513  {
6514  // Display "Message of the day" window
6515  new MotdWindow(motd, title);
6516  // Release memory for window title
6517  delete[] title;
6518  }
6519 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
6520  // Display "Message of the day" window
6521  new MotdWindow(motd, ts.c_str());
6522 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
6523  // Release memory for raw article content
6524  enc_free((void*) motd);
6525  core_free((void*) p);
6526  }
6527  if(!stateMachine(EVENT_MOTD_VIEW_EXIT))
6528  {
6529  PRINT_ERROR("Error in main window state machine");
6530  }
6531  }
6532 }
6533 
6534 
6535 // =============================================================================
6536 // Main window view article callback
6537 // (must be locked)
6538 //
6539 // Note: The parameter mid must be specified without angle brackets!
6540 
6541 void MainWindow::viewArticle(int action, const char* mid)
6542 {
6543  static const char debug_prefix[] = "Try to fetch article: ";
6544  int rv = -1;
6545  std::ostringstream titleString;
6546  const char* article;
6547  char* sbuf;
6548  std::size_t len;
6549 
6550  if(UI_CB_START == action)
6551  {
6552  if(!stateMachine(EVENT_A_VIEW)) { rv = 1; }
6553  else if(NULL != mid)
6554  {
6555  // Start core to do the work
6556  UI_STATUS(S("Get article ..."));
6557  UI_BUSY();
6558  // Add angle brackets
6559  mid_a = new char[std::strlen(mid) + (std::size_t) 3];
6560  mid_a[0] = '<';
6561  std::strcpy(&mid_a[1], mid);
6562  std::strcat(mid_a, ">");
6563  if(main_debug)
6564  {
6565  len = std::strlen(MAIN_ERR_PREFIX);
6566  len += std::strlen(debug_prefix);
6567  len += std::strlen(mid_a);
6568  sbuf = new char[len + (std::size_t) 1];
6569  std::strcpy(sbuf, MAIN_ERR_PREFIX);
6570  std::strcat(sbuf, debug_prefix);
6571  std::strcat(sbuf, mid_a);
6572  print_error(sbuf);
6573  delete[] sbuf;
6574  }
6575  rv = core_get_article_by_mid(mid_a, UI_CB_COOKIE_ARTICLE);
6576  }
6577  }
6578  else
6579  {
6580  // Get result
6581  core_mutex_lock();
6582  rv = data.result;
6584  // Free memory for Message-ID with angle brackets
6585  delete[] mid_a;
6586  }
6587 
6588  // Check return value (positive value means "in progress")
6589  if(0 >= rv)
6590  {
6591  UI_READY();
6592  if(0 > rv)
6593  {
6594  UI_STATUS(S("Downloading article failed."));
6595  SC("Do not use non-ASCII for the translation of this item")
6596  fl_message_title(S("Error"));
6597  fl_alert("%s", S("Article not found"));
6598  }
6599  else
6600  {
6601  // Success
6602  core_mutex_lock();
6603  article = (const char*) data.data;
6605  UI_STATUS(S("Article successfully downloaded."));
6606 
6607  // Create Unicode title for article window
6608  SC("")
6609  SC("Do not use characters for the translation that cannot be")
6610  SC("converted to the ISO 8859-1 character set for this item.")
6611  SC("Leave the original string in place if in doubt.")
6612  titleString << S("Article") << std::flush;
6613  SC("")
6614 
6615  // Attention:
6616  //
6617  // const char* title = titleString.str().c_str()
6618  //
6619  // creates 'title' with undefined value because the compiler is allowed
6620  // to free the temporary string object returned by 'str()' immediately
6621  // after the assignment!
6622  // Assigning names to temporary string objects forces them to stay in
6623  // memory as long as their names go out of scope (this is what we
6624  // need).
6625  const std::string& ts = titleString.str();
6626 
6627  // Set "Article" window title
6628 #if CFG_USE_XSI && !CFG_NLS_DISABLE
6629  // Convert window title to the encoding required by the window manager
6630  // (WM)
6631  // It is assumed that the WM can display either ISO 8859-1 or UTF-8
6632  // encoded window titles.
6633  const char* title;
6634  title = gui_utf8_iso(ts.c_str());
6635  if(NULL != title)
6636  {
6637  // Display "Article" window
6638  new ArticleWindow(article, title);
6639  // Release memory for window title
6640  delete[] title;
6641  }
6642 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
6643  // Display "Article" window
6644  new ArticleWindow(article, ts.c_str());
6645 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
6646  // Release memory for raw article content
6647  core_free((void*) article);
6648  }
6649  if(!stateMachine(EVENT_A_VIEW_EXIT))
6650  {
6651  PRINT_ERROR("Error in main window state machine");
6652  }
6653  }
6654 }
6655 
6656 
6657 // =============================================================================
6658 // Main window view article source code callback
6659 // (must be locked)
6660 
6661 void MainWindow::viewSrc(int action)
6662 {
6663  int rv = -1;
6664  std::ostringstream titleString;
6665  const char* article;
6666 
6667  if(UI_CB_START == action)
6668  {
6669  if(!stateMachine(EVENT_SRC_VIEW)) { rv = 1; }
6670  else if(NULL != currentArticleHE)
6671  {
6672  // Start core to do the work
6673  UI_STATUS(S("Get article source code ..."));
6674  UI_BUSY();
6675  rv = core_get_article(&currentArticleHE->anum, UI_CB_COOKIE_SRC);
6676  }
6677  }
6678  else
6679  {
6680  // Get result
6681  core_mutex_lock();
6682  rv = data.result;
6684  }
6685 
6686  // Check return value (positive value means "in progress")
6687  if(0 >= rv)
6688  {
6689  UI_READY();
6690  if(0 > rv) { UI_STATUS(S("Downloading article source code failed.")); }
6691  else
6692  {
6693  // Success
6694  core_mutex_lock();
6695  article = (const char*) data.data;
6697  UI_STATUS(S("Article source code successfully downloaded."));
6698 
6699  // Create Unicode title for article source code window
6700  SC("")
6701  SC("Do not use characters for the translation that cannot be")
6702  SC("converted to the ISO 8859-1 character set for this item.")
6703  SC("Leave the original string in place if in doubt.")
6704  titleString << S("Article source code") << std::flush;
6705  SC("")
6706 
6707  // Attention:
6708  //
6709  // const char* title = titleString.str().c_str()
6710  //
6711  // creates 'title' with undefined value because the compiler is allowed
6712  // to free the temporary string object returned by 'str()' immediately
6713  // after the assignment!
6714  // Assigning names to temporary string objects forces them to stay in
6715  // memory as long as their names go out of scope (this is what we
6716  // need).
6717  const std::string& ts = titleString.str();
6718 
6719  // Set "Article source code" window title
6720 #if CFG_USE_XSI && !CFG_NLS_DISABLE
6721  // Convert window title to the encoding required by the window manager
6722  // (WM)
6723  // It is assumed that the WM can display either ISO 8859-1 or UTF-8
6724  // encoded window titles.
6725  const char* title;
6726  title = gui_utf8_iso(ts.c_str());
6727  if(NULL != title)
6728  {
6729  // Display "Article source code" window
6730  new ArticleSrcWindow(article, title);
6731  // Release memory for window title
6732  delete[] title;
6733  }
6734 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
6735  // Display "Article source code" window
6736  new ArticleSrcWindow(article, ts.c_str());
6737 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
6738  // Release memory for raw article content
6739  core_free((void*) article);
6740  }
6741  if(!stateMachine(EVENT_SRC_VIEW_EXIT))
6742  {
6743  PRINT_ERROR("Error in main window state machine");
6744  }
6745  }
6746 }
6747 
6748 
6749 // =============================================================================
6750 // Main window article compose callback
6751 // (must be locked)
6752 //
6753 // reply / super Action
6754 // ----------------------------------------------
6755 // false / false Create a new message/thread
6756 // false / true Create a cancel control message
6757 // true / false Create a followup message
6758 // true / true Create a superseding message
6759 
6760 void MainWindow::articleCompose(bool reply, bool super)
6761 {
6762  const char* fqdn = config[CONF_FQDN].val.s;
6763  std::ostringstream titleString;
6764  char* p = NULL;
6765  char* q = NULL;
6766  const char* cq = NULL;
6767  std::size_t i = 0;
6768  char* sig_delim;
6769  char* sig = NULL;
6770  struct core_article_header* hdr = NULL;
6771  char* from = NULL;
6772  std::size_t len;
6773  Fl_Text_Buffer header;
6774  const char* msgid = NULL;
6775  const char* ckey1 = NULL;
6776  const char* ckey = NULL;
6777  const char* subject;
6778  const char* datetime;
6779  bool headerOK = true;
6780  int rv;
6781  int start;
6782  int end;
6783  unsigned int c = (unsigned int) '\n';
6784  int pos;
6785  const char* field_name;
6786  const char* field;
6787  const char* field2;
6788  bool insertedMessageID = false;
6789  int inspos;
6790  bool abort = true;
6791  bool error = true;
6792 
6793  // Allow only one composer at a time
6794  if(!stateMachine(EVENT_COMPOSE)) { return; }
6795 
6796  if(!groupList->value())
6797  {
6798  SC("Do not use non-ASCII for the translation of this item")
6799  fl_message_title(S("Error"));
6800  fl_alert("%s", S("No group selected"));
6801  }
6802  else if((reply || super) && NULL == currentArticleHE)
6803  {
6804  SC("Do not use non-ASCII for the translation of this item")
6805  fl_message_title(S("Error"));
6806  fl_alert("%s", S("No article selected"));
6807  }
6808  else
6809  {
6810  if(reply || super) { hdr = currentArticleHE->header; }
6811  abort = false;
6812  }
6813  if(!abort)
6814  {
6815  // Check for special exception Fup2 poster
6816  if(reply && !super && NULL != hdr->fup2)
6817  {
6818  if(!std::strcmp("poster", hdr->fup2))
6819  {
6820  SC("Do not use non-ASCII for the translation of this item")
6821  fl_message_title(S("Warning"));
6822  rv = fl_choice("%s", S("Cancel"),
6823  S("OK"),
6824  S("Ignore"),
6825  S("Really execute Followup-To to poster?"));
6826  if(1 == rv) { sendEmail(); } // OK
6827  if(!rv || 1 == rv) { abort = true; }
6828  }
6829  }
6830  }
6831  if(!abort)
6832  {
6833  if(reply)
6834  {
6835  // Prepare current article content for citation
6836  if(currentArticle->selected())
6837  {
6838  // Some text was selected, use only this part
6839  p = currentArticle->selection_text();
6840  if(NULL != p) { cq = p; }
6841  }
6842  else
6843  {
6844  // Use complete article body
6845  p = currentArticle->text();
6846  if(NULL != p)
6847  {
6848  // Strip header
6849  while('_' != p[i++]);
6850  while('|' != p[i++]);
6851  while('|' != p[i++]);
6852  q = &p[++i];
6853  cq = q;
6854  // Strip signature
6855  while(1)
6856  {
6857  // Attention: Overloaded and both prototypes different than in C!
6858  sig_delim = std::strstr(q, "\n-- \n");
6859  if(NULL == sig_delim) { break; }
6860  else { sig = q = &sig_delim[1]; }
6861  }
6862  if(NULL != sig) { sig[0] = 0; }
6863  }
6864  }
6865  // Extract author name
6866  len = std::strlen(hdr->from);
6867  from = new char[++len];
6868  std::strcpy(from, hdr->from);
6869  stripAngleAddress(from);
6870  }
6871  else
6872  {
6873  if(super)
6874  {
6875  // Prepare default cancel reason text
6876  cq = "Reason for cancel unknown.\n";
6877  }
6878  else if(std::strlen(config[CONF_INITIAL_GREETING].val.s))
6879  {
6880  // Prepare initial greeting (if configured)
6882  }
6883  }
6884 
6885  // Create message header
6886  //
6887  // According to RFC 5536 the following rules are applied:
6888  // - Arbitrary ordering of header fields is allowed => We use common one
6889  // - Header field name must be followed by a colon and a space => We do so
6890  //
6891  // According to RFC 5537 the following rules are applied:
6892  // - Mandatory fields for proto article: From, Newsgroups, Subject
6893  // => We create at least these header fields
6894  // - The header field References must be no longer than 998 octets
6895  // after unfolding => We trim the references if this is the case
6896  header.append("Path: not-for-mail\n");
6897  if(std::strlen(fqdn))
6898  {
6899  header.append("Message-ID: ");
6900  msgid = core_get_msgid(fqdn);
6901  if(NULL == msgid) { headerOK = false; }
6902  else
6903  {
6904  header.append(msgid);
6905  insertedMessageID = true;
6906  }
6907  // Memory is released later because of Cancel-Lock!
6908  header.append("\n");
6909  }
6910  if(!std::strlen(config[CONF_FROM].val.s))
6911  {
6912  SC("Do not use non-ASCII for the translation of this item")
6913  fl_message_title(S("Error"));
6914  fl_alert("%s", S("From: header field not configured"));
6915  headerOK = false;
6916  }
6917  else
6918  {
6919  field_name = "From: ";
6920  field = enc_create_name_addr(config[CONF_FROM].val.s,
6921  std::strlen(field_name));
6922  if(NULL == field) { headerOK = false; }
6923  else
6924  {
6925  header.append(field_name);
6926  header.append(field);
6927  header.append("\n");
6928  // Only allow supersede or cancel for own messages
6929  if(super)
6930  {
6931  field2 = enc_create_name_addr(hdr->from,
6932  std::strlen(field_name));
6933  if(NULL == field2) { headerOK = false; }
6934  else
6935  {
6936  if(std::strcmp(field, field2))
6937  {
6938  SC("Do not use non-ASCII for the translation of this item")
6939  fl_message_title(S("Error"));
6940  fl_alert("%s", S("Supersede or cancel not allowed"));
6941  headerOK = false;
6942  }
6943  enc_free((void*) field2);
6944  }
6945  }
6946  enc_free((void*) field);
6947  }
6948  }
6949  header.append("Newsgroups: ");
6950  if(!super && !reply) { header.append(currentGroup->name); }
6951  else
6952  {
6953  len = 12;
6954  // Check for Fup2 header field
6955  rv = 0;
6956  if(!super && NULL != hdr->fup2)
6957  {
6958  if(std::strcmp("poster", hdr->fup2))
6959  {
6960  SC("Do not use non-ASCII for the translation of this item")
6961  fl_message_title(S("Note"));
6962  fl_message("%s", S("Followup-To specified groups executed"));
6963  header.append(hdr->fup2);
6964  len += std::strlen(hdr->fup2);
6965  rv = 1;
6966  }
6967  }
6968  if(!rv)
6969  {
6970  // Not found => Use current group list
6971  i = 0;
6972  do
6973  {
6974  if(i) { header.append(","); }
6975  ++len;
6976  header.append(hdr->groups[i]);
6977  len += std::strlen(hdr->groups[i]);
6978  }
6979  while(NULL != hdr->groups[++i]);
6980  }
6981  // Verify length but never fold "Newsgroups" header field
6982  if((std::size_t) 998 < len) { headerOK = false; }
6983  }
6984  header.append("\n");
6985  header.append("Subject: ");
6986  if(reply)
6987  {
6988  if(super) { subject = hdr->subject; }
6989  else
6990  {
6991  header.append("Re: ");
6992  subject = gui_check_re_prefix(hdr->subject);
6993  }
6994  header.append(subject);
6995  }
6996  else if(super)
6997  {
6998  // Old behaviour until 0.14
6999  //header.append(hdr->subject);
7000  // New behaviour since 0.15 (backward compatible to RFC 1036 syntax)
7001  if(NULL == hdr->msgid) { headerOK = false; }
7002  else
7003  {
7004  header.append("cmsg cancel ");
7005  header.append(hdr->msgid);
7006  }
7007  }
7008  header.append("\n");
7009  if(!super)
7010  {
7011  // Strip substring starting with "(was:" matched case insensitive
7012  rv = header.search_forward(0, "Subject:", &start, 1);
7013  if(1 == rv)
7014  {
7015  // Name match found => Verify that it starts at BOL
7016  if(0 < start) { c = header.char_at(start - 1); }
7017  if((unsigned int) '\n' == c)
7018  {
7019  // Yes => Search for EOL
7020  rv = header.findchar_forward(start, (unsigned int) '\n', &end);
7021  if(1 == rv)
7022  {
7023  rv = header.search_forward(start, "(was:", &pos, 0);
7024  if(1 == rv)
7025  {
7026  header.remove(pos, end);
7027  if(pos)
7028  {
7029  if((unsigned int) ' ' == header.char_at(pos - 1))
7030  {
7031  header.remove(pos - 1, pos);
7032  }
7033  }
7034  }
7035  }
7036  }
7037  }
7038  }
7039  header.append("Date: ");
7040  datetime = core_get_datetime(0);
7041  if(NULL == datetime) { headerOK = false; }
7042  else { header.append(datetime); }
7043  header.append("\n");
7044  // --- Optional header fields ---
7045  if(insertedMessageID)
7046  {
7047  // Create Cancel-Lock header if we have created the Message-ID
7050  if(NULL != ckey1 || NULL != ckey)
7051  {
7052  header.append("Cancel-Lock: ");
7053  if(NULL != ckey1) { header.append(ckey1); }
7054  if(NULL != ckey1 && NULL != ckey) { header.append(" " ); }
7055  if(NULL != ckey) { header.append(ckey); }
7056  header.append("\n");
7057  }
7058  core_free((void*) ckey);
7059  core_free((void*) ckey1);
7060  // RFC 5536 requires that this header is inserted at injection.
7061  // According to RFC 5537 an injecting agent is not allowed to add this
7062  // header field to a proto-article that already contains "Message-ID"
7063  // and "Date" header-fields. Therefore, if we have added these two
7064  // header-fields, we add "Injection-Date" too.
7065  header.append("Injection-Date: ");
7066  header.append(datetime);
7067  header.append("\n");
7068  }
7069  core_free((void*) datetime);
7070  core_free((void*) msgid); // Released here because of Cancel-Lock
7071  if(super)
7072  {
7073  msgid = hdr->msgid;
7074  if(NULL == msgid) { headerOK = false; }
7075  else
7076  {
7077  if(reply)
7078  {
7079  header.append("Supersedes: ");
7080  header.append(msgid);
7081  header.append("\n");
7082  }
7083  else
7084  {
7085  header.append("Control: cancel ");
7086  header.append(msgid);
7087  header.append("\n");
7088  }
7089  if(insertedMessageID)
7090  {
7091  // Create Cancel-Key header if we have created the Message-ID
7094  if(NULL != ckey1 || NULL != ckey)
7095  {
7096  header.append("Cancel-Key: ");
7097  if(NULL != ckey1) { header.append(ckey1); }
7098  if(NULL != ckey1 && NULL != ckey) { header.append(" " ); }
7099  if(NULL != ckey) { header.append(ckey); }
7100  header.append("\n");
7101  }
7102  core_free((void*) ckey1);
7103  core_free((void*) ckey);
7104  }
7105  }
7106  }
7107  if(std::strlen(config[CONF_REPLYTO].val.s))
7108  {
7109  field_name = "Reply-To: ";
7110  field = enc_create_name_addr(config[CONF_REPLYTO].val.s,
7111  std::strlen(field_name));
7112  if(NULL == field) { headerOK = false; }
7113  else
7114  {
7115  header.append(field_name);
7116  header.append(field);
7117  header.append("\n");
7118  enc_free((void*) field);
7119  }
7120  }
7121  // According to RFC 5537 the maximum length of the References header
7122  // field must be trimmed to 998 octets. Because Message-IDs are not
7123  // allowed to contain MIME encoded words, folding of this header
7124  // field is never required.
7125  if(reply)
7126  {
7127  len = 0;
7128  inspos = 0;
7129  // Add new reference for parent if not superseding
7130  if(!(NULL == hdr->refs && super))
7131  {
7132  header.append("References: ");
7133  inspos = header.length();
7134  len = 12;
7135  header.append("\n");
7136  }
7137  if(!super)
7138  {
7139  header.insert(inspos, hdr->msgid);
7140  len += std::strlen(hdr->msgid);
7141  }
7142  if(NULL != hdr->refs)
7143  {
7144  if(NULL != hdr->refs[0])
7145  {
7146  // Count first previous reference
7147  ++len;
7148  len += std::strlen(hdr->refs[0]);
7149  // Insert previous references (backward, beginning with last one)
7150  i = 0;
7151  while(NULL != hdr->refs[i]) { ++i; }
7152  while(--i)
7153  {
7154  len += std::strlen(hdr->refs[i]);
7155  if(UI_HDR_BUFSIZE < ++len) { break; }
7156  else
7157  {
7158  header.insert(inspos, " ");
7159  header.insert(inspos, hdr->refs[i]);
7160  }
7161  }
7162  // Always insert first previous reference
7163  header.insert(inspos, " ");
7164  header.insert(inspos, hdr->refs[0]);
7165  }
7166  }
7167  }
7168  if(std::strlen(config[CONF_ORGANIZATION].val.s))
7169  {
7170  header.append("Organization: ");
7171  header.append(config[CONF_ORGANIZATION].val.s);
7172  header.append("\n");
7173  }
7174  if(config[CONF_ENABLE_UAGENT].val.i)
7175  {
7176  header.append("User-Agent: " CFG_NAME "/" CFG_VERSION
7177  " (for " CFG_OS ")\n");
7178  }
7179  // Insert MIME header field
7180  // Note: It is mandatory for a MIME conformant client to send this header
7181  // field with all messages!
7182  header.append("MIME-Version: 1.0\n");
7183  // Insert header/body separator (empty line)
7184  header.append("\n");
7185 
7186  // Ensure that header was successfully created
7187  if(true != headerOK)
7188  {
7189  SC("Do not use non-ASCII for the translation of this item")
7190  fl_message_title(S("Error"));
7191  fl_alert("%s", S("Creating message header failed"));
7192  }
7193  else
7194  {
7195  const char* header_text;
7196  header_text = header.text();
7197  if(NULL != header_text)
7198  {
7199  // Create Unicode title for compose window
7200  SC("")
7201  SC("Do not use characters for the translation that cannot be")
7202  SC("converted to the ISO 8859-1 character set for the items")
7203  SC("in this section.")
7204  SC("Leave the original strings in place if in doubt.")
7205  if(reply)
7206  {
7207  titleString << S("Compose followup or reply") << std::flush;
7208  }
7209  else { titleString << S("Compose new article") << std::flush; }
7210  SC("")
7211  // Attention:
7212  //
7213  // const char* title = titleString.str().c_str()
7214  //
7215  // creates 'title' with undefined value because the compiler is allowed
7216  // to free the temporary string object returned by 'str()' immediately
7217  // after the assignment!
7218  // Assigning names to temporary string objects forces them to stay in
7219  // memory as long as their names go out of scope (this is what we
7220  // need).
7221  const std::string& ts = titleString.str();
7222 
7223  // Construct compose window
7224 #if CFG_USE_XSI && !CFG_NLS_DISABLE
7225  // Convert window title to the encoding required by the window manager
7226  // (WM)
7227  // It is assumed that the WM can display either ISO 8859-1 or UTF-8
7228  // encoded window titles.
7229  const char* title;
7230  title = gui_utf8_iso(ts.c_str());
7231  if(NULL != title)
7232  {
7233  // Construct article composer
7234  composeWindow = new ComposeWindow(title, header_text, cq, from,
7235  hdr, super);
7236  error = false;
7237  // Release memory for window title
7238  delete[] title;
7239  }
7240 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
7241  // Construct article composer
7242  composeWindow = new ComposeWindow(ts.c_str(), header_text, cq, from,
7243  hdr, super);
7244  error = false;
7245 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
7246  }
7247  std::free((void*) header_text);
7248  }
7249  }
7250 
7251  // Release memory
7252  if(NULL != from) { delete[] from; }
7253  std::free((void*) p);
7254 
7255  // Verify that compose windows was constructed
7256  if(error)
7257  {
7258  if(!stateMachine(EVENT_COMPOSE_EXIT))
7259  {
7260  PRINT_ERROR("Error in main window state machine");
7261  }
7262  }
7263 }
7264 
7265 
7266 // =============================================================================
7267 // Main window article posting callback
7268 // (must be locked)
7269 //
7270 // The pointer \e article must point to a memory block allocated by \c malloc()
7271 // and this function will call \c free(article) .
7272 
7273 void MainWindow::articlePost(int action, const char* article)
7274 {
7275  int rv = -1;
7276  const char* p;
7277  enum fm
7278  {
7279  FM_DEFAULT,
7280  FM_CORE,
7281  FM_EXT
7282  }
7283  free_method;
7284 
7285  free_method = FM_DEFAULT;
7286  if(UI_CB_START == action)
7287  {
7288  if(!stateMachine(EVENT_POST)) { rv = 1; }
7289  else
7290  {
7291  UI_STATUS(S("Post article ..."));
7292  UI_BUSY();
7293  // Convert article content line breaks from POSIX form to canonical
7294  // (RFC 822) form
7295  p = core_convert_posix_to_canonical(article);
7296  if(NULL == p)
7297  {
7298  SC("Do not use non-ASCII for the translation of this item")
7299  fl_message_title(S("Error"));
7300  fl_alert("%s", S("Conversion from local to canonical form failed"));
7301  }
7302  else
7303  {
7304  // Success
7305  if(p != article)
7306  {
7307  std::free((void*) article);
7308  article = p;
7309  free_method = FM_CORE;
7310  }
7311  // Call external postprocessor (while blocking UI)
7312  // The body of the article always has Unicode format
7313  p = ext_pp_filter(article);
7314  if(NULL == p)
7315  {
7316  SC("Do not use non-ASCII for the translation of this item")
7317  fl_message_title(S("Error"));
7318  fl_alert("%s", S("External postprocessor failed"));
7319  }
7320  else
7321  {
7322  // Success
7323  if(p != article)
7324  {
7325  if(FM_CORE == free_method) { core_free((void*) article); }
7326  else { std::free((void*) article); }
7327  article = p;
7328  free_method = FM_EXT;
7329  }
7330  // Start core to do the work
7331  rv = core_post_article(article, UI_CB_COOKIE_POST);
7332  }
7333  }
7334  }
7335  // Release memory
7336  switch(free_method)
7337  {
7338  case FM_CORE:
7339  {
7340  core_free((void*) article);
7341  break;
7342  }
7343  case FM_EXT:
7344  {
7345  ext_free((void*) article);
7346  break;
7347  }
7348  default:
7349  {
7350  std::free((void*) article);
7351  break;
7352  }
7353  }
7354  }
7355  else
7356  {
7357  // Get result
7358  core_mutex_lock();
7359  rv = data.result;
7361  }
7362 
7363  // Check return value (positive value means "in progress")
7364  if(0 >= rv)
7365  {
7366  UI_READY();
7367  if(!stateMachine(EVENT_POST_EXIT))
7368  {
7369  PRINT_ERROR("Error in main window state machine");
7370  }
7371  if(0 > rv)
7372  {
7373  UI_STATUS(S("Posting article failed."));
7374  SC("Do not use non-ASCII for the translation of this item")
7375  fl_message_title(S("Error"));
7376  fl_alert("%s", S("Posting article failed."));
7377  }
7378  else
7379  {
7380  // Success
7381  UI_STATUS(S("Article successfully posted."));
7382  if(NULL != composeWindow)
7383  {
7384  delete composeWindow;
7385  composeWindow = NULL;
7386  // Call this after EVENT_POST_EXIT
7387  composeComplete();
7388  }
7389  }
7390  }
7391 }
7392 
7393 
7394 // =============================================================================
7395 // Main window article tree search (for Message-ID)
7396 // The target article is selected, if found.
7397 //
7398 // target_mid must point to the first character after the opening angle bracket
7399 // tlen must be the size of the Message-ID without angle brackets.
7400 
7401 Fl_Tree_Item* MainWindow::searchSelectArticle(const char* target_mid,
7402  std::size_t tlen)
7403 {
7404  Fl_Tree_Item* ti;
7405  Fl_Tree_Item* oi;
7406  const char* mid;
7407  bool abort = false;
7408  int i;
7409 
7410  ti = articleTree->root();
7411  // Assignment in truth expression is intended
7412  while(NULL != (ti = articleTree->next(ti)))
7413  {
7414  mid = ((core_hierarchy_element*) ti->user_data())->header->msgid;
7415  if(std::strlen(mid) != tlen + (std::size_t) 2)
7416  {
7417  continue;
7418  }
7419  // Note: The Message-ID from header has angle brackets
7420  if(!std::strncmp(&mid[1], target_mid, tlen))
7421  {
7422  // Open branch containing target article
7423  oi = ti;
7424  while(articleTree->root() != oi)
7425  {
7426  if(!articleTree->open(oi))
7427  {
7428  articleTree->open(oi, 1);
7429  }
7430  for(i = 0; i < articleTree->root()->children(); ++i)
7431  {
7432  if(articleTree->root()->child(i) == oi)
7433  {
7434  abort = true;
7435  break;
7436  }
7437  }
7438  if(abort) { break; }
7439  oi = articleTree->prev(oi);
7440  }
7441  // Select last viewed article
7442  articleTree->set_item_focus(ti);
7443  articleTree->deselect_all();
7444  articleTree->select(ti, 1);
7445  Fl::focus(articleTree);
7446  break;
7447  }
7448  }
7449 
7450  return(ti);
7451 }
7452 
7453 
7454 // =============================================================================
7455 // Store MIME entity to file
7456 
7457 void MainWindow::storeMIMEEntityToFile(const char* uri, const char* msgid)
7458 {
7459  const char* link = NULL;
7460  const char* start = NULL;
7461  std::size_t len = 0;
7462  const char* p;
7463  unsigned int i;
7464  int error = 1;
7465  char* name = NULL;
7466  const char* suggest = NULL;
7467 
7468  // Check whether link points to our MIME entities (ignore entity index)
7469  link = gui_create_link_to_entity(msgid, 0);
7470  if(NULL != link && link[0])
7471  {
7472  // Attention: 'link' is created with angle brackets and dummy entity index
7473  start = &link[1];
7474  p = std::strrchr(start, (int) '/');
7475  if(NULL != p)
7476  {
7477  len = (size_t) (p - start);
7478  error = 0;
7479  }
7480  }
7481  if(main_debug)
7482  {
7483  if(error)
7484  {
7485  fprintf(stderr, "%s: %sCannot check link to MIME entity (bug)\n",
7486  CFG_NAME, MAIN_ERR_PREFIX);
7487  }
7488  else
7489  {
7490  fprintf(stderr, "%s: %sClick on link to MIME entity detected:\n",
7491  CFG_NAME, MAIN_ERR_PREFIX);
7492  fprintf(stderr, "%s: %s%s (URI)\n", CFG_NAME, MAIN_ERR_PREFIX, uri);
7493  fprintf(stderr, "%s: %s", CFG_NAME, MAIN_ERR_PREFIX);
7494  for(i = 0; len > (std::size_t) i; ++i)
7495  {
7496  fprintf(stderr, "%c", start[i]);
7497  }
7498  fprintf(stderr, " (Compare, Length: %u)\n", (unsigned int) len);
7499  }
7500  }
7501  // Check whether links differ before last "/"
7502  if(error || std::strncmp(uri, start, len))
7503  {
7504  // Yes (or error)
7505  SC("Do not use non-ASCII for the translation of this item")
7506  fl_message_title(S("Error"));
7507  fl_alert("%s", S("Link target not found or not supported"));
7508  }
7509  else
7510  {
7511  // Attention: Overloaded and both prototypes different than in C!
7512  p = std::strrchr(uri, (int) '/');
7513  if(NULL == p || 1 != std::sscanf(++p, "%u", &i))
7514  {
7515  PRINT_ERROR("Extracting MIME entity number from file URI failed");
7516  }
7517  else
7518  {
7519  enc_mime_cte cte = ENC_CTE_UNKNOWN;
7520  const char* entity_body = mimeData->part(i, &cte, NULL);
7521  if(NULL != entity_body)
7522  {
7523  SC("Do not use characters for the translation that cannot be")
7524  SC("converted to the ISO 8859-1 character set for this item.")
7525  SC("Leave the original string in place if in doubt.")
7526  const char* title = S("Save attachment");
7527  const char* homedir = core_get_homedir();
7528  if(NULL != homedir)
7529  {
7530  if(NULL != mimeData->filename(i))
7531  {
7532  // Use filename from Content-Disposition header field
7533  name = (char*) std::malloc(std::strlen(homedir)
7534  + std::strlen(mimeData->filename(i))
7535  + (size_t) 2);
7536  if(NULL != name)
7537  {
7538  suggest = name;
7539  std::strcpy(name, homedir);
7540  std::strcat(name, "/");
7541  std::strcat(name, mimeData->filename(i));
7542  }
7543  }
7544  else
7545  {
7546  // Use generic filename
7547  suggest = core_suggest_pathname();
7548  }
7549  if (NULL != suggest)
7550  {
7551  fl_file_chooser_ok_label(S("Save"));
7552  const char* pathname =
7553  fl_file_chooser(title, "*", suggest, 0);
7554  if(NULL != pathname)
7555  {
7556  // Convert pathname encoding to locale
7557  const char* pn =
7559  if(NULL == pn)
7560  {
7561  SC("Do not use non-ASCII for the translation of this item")
7562  fl_message_title(S("Error"));
7563  fl_alert("%s",
7564  S("Pathname conversion to locale codeset failed"));
7565  }
7566  else
7567  {
7568  // Decode Content-Transfer-Encoding and save body to file
7569  int rv = enc_mime_save_to_file(pn, cte, entity_body);
7570  if(rv)
7571  {
7572  SC("Do not use non-ASCII for the translation of this item")
7573  fl_message_title(S("Error"));
7574  fl_alert("%s", S("Operation failed"));
7575  }
7576  core_free((void*) pn);
7577  }
7578  }
7579  }
7580  core_free((void*) homedir);
7581  }
7582  }
7583  }
7584  }
7585  std::free((void*) link);
7586  std::free((void*) suggest);
7587 }
7588 
7589 
7590 // =============================================================================
7591 // Main window hyperlink handling callback
7592 
7593 void MainWindow::hyperlinkHandler(int pos)
7594 {
7595  const char* mailto = "mailto:";
7596  const char* news = "news:";
7597  const char* nntp = "nntp:";
7598  const char* file = "file:";
7599  char* uri = NULL;
7600  int rv;
7601  int i = pos;
7602  int start = pos;
7603  int end = start;
7604  std::size_t len;
7605  Fl_Tree_Item* ti;
7606  bool abort = false;
7607  std::size_t clen;
7608  std::size_t gi;
7609  core_anum_t ref_i;
7610  const char* p;
7611  char* q;
7612  int invalid;
7613 
7614  //std::printf("Debug: Hyperlink clicked at position: %d\n", pos);
7615  if(currentArticle && currentStyle)
7616  {
7617  // Extract complete URI
7618  while(i <= pos)
7619  {
7620  i = currentArticle->prev_char(i);
7621  if(currentStyle->char_at(i) == hyperlinkStyle) { start = i; }
7622  else { break; }
7623  }
7624  i = pos;
7625  while(i >= pos)
7626  {
7627  i = currentArticle->next_char(i);
7628  if(currentStyle->char_at(i) == hyperlinkStyle) { end = i; }
7629  else { ++end; break; }
7630  }
7631  uri = currentArticle->text_range(start, end);
7632  if(NULL != uri)
7633  {
7634  // Check for "nntp:" URI
7635  len = std::strlen(nntp);
7636  if(std::strlen(uri) >= len && !std::strncmp(uri, nntp, len))
7637  {
7638  // Yes => Not supported
7639  PRINT_ERROR("Multi server support missing for nntp URI type");
7640  SC("Do not use non-ASCII for the translation of this item")
7641  fl_message_title(S("Error"));
7642  fl_alert("%s", S("URI type is not supported"));
7643  }
7644  else
7645  {
7646  // Check for "news:" URI
7647  // According to RFC 1738 this specifies a newsgroup or a Message-ID
7648  // An asterisk is allowed as wildcard => This is not supported
7649  // According to RFC 5538 a server can be specified and wildmat
7650  // syntax is allowed => Both options are not supported
7651  len = std::strlen(news);
7652  if(std::strlen(uri) >= len && !std::strncmp(uri, news, len))
7653  {
7654  // Accept empty <host> for <authority>: "news:///"
7655  if(!std::strncmp(&uri[len], "///", 3))
7656  {
7657  std::memmove(&uri[len], &uri[len + (size_t) 3],
7658  std::strlen(&uri[len + (size_t) 3])
7659  + (size_t) 1);
7660  }
7661  if(std::strlen(uri) == len)
7662  {
7663  SC("Do not use non-ASCII for the translation of this item")
7664  fl_message_title(S("Error"));
7665  fl_alert("%s", S("Invalid URI format"));
7666  }
7667  // Check for unsupported format with <authority>
7668  else if(std::strchr(&uri[len], (int) '/'))
7669  {
7670  SC("Do not use non-ASCII for the translation of this item")
7671  fl_message_title(S("Error"));
7672  fl_alert("%s",
7673  S("Server specification in URI not supported"));
7674  }
7675  // Check whether URI points to newsgroup or Message-ID
7676  // Attention: Overloaded and both prototypes different than in C!
7677  else if(!std::strchr(&uri[len], (int) '@'))
7678  {
7679  // Select newsgroup
7680  if(std::strchr(&uri[len], (int) '*')
7681  || std::strchr(&uri[len], (int) '?'))
7682  {
7683  SC("Do not use non-ASCII for the translation of this item")
7684  fl_message_title(S("Error"));
7685  fl_alert("%s", S("URI doesn't specify a single group"));
7686  }
7687  else
7688  {
7689  rv = enc_percent_decode(&uri[len], 1);
7690  if(0 > rv)
7691  {
7692  SC("Do not use non-ASCII for the translation of this item")
7693  fl_message_title(S("Error"));
7694  fl_alert("%s", S("Invalid URI format"));
7695  }
7696  else
7697  {
7698  abort = true;
7699  if(group_list)
7700  {
7701  for(gi = 0; gi < group_num; ++gi)
7702  {
7703  clen = std::strlen(&uri[len]);
7704  if(std::strlen(group_list[gi].name) == clen)
7705  {
7706  if(!strncmp(&uri[len], group_list[gi].name,
7707  clen))
7708  {
7709  if((std::size_t) INT_MAX >= ++gi)
7710  {
7711  groupList->select((int) gi);
7712  groupSelect(UI_CB_START, (int) gi);
7713  abort = false;
7714  }
7715  break;
7716  }
7717  }
7718  }
7719  }
7720  }
7721  if(abort)
7722  {
7723  fl_message_title(S("Note"));
7724  rv = fl_choice("%s", S("No"),
7725  S("Yes"), NULL,
7726  // Don't wrap this line because of NLS macro!
7727  S("URI specifies a group that is not subscribed.\nSubscribe now?"));
7728  if(rv)
7729  {
7730  rv = groupStateMerge();
7731  if(!rv)
7732  {
7733  rv = core_subscribe_group(&uri[len]);
7734  if(!rv)
7735  {
7736  UI_STATUS(S("Group subscription stored."));
7737  // This check should silence code check tools
7738  // ('mainWindow' is always present)
7739  if(NULL != mainWindow)
7740  {
7741  // Import new group list
7742  mainWindow->groupListImport();
7743  }
7744  // Direct selection of the new group here is
7745  // not possible without core command queue.
7746  fl_message("%s",
7747  // Don't wrap this line because of NLS macro!
7748  S("Click URI again after operation is complete"));
7749  }
7750  }
7751  }
7752  }
7753  }
7754  }
7755  else
7756  {
7757  // Select article that corresponds to Message-ID
7758  rv = enc_percent_decode(&uri[len], 1);
7759  if(0 > rv)
7760  {
7761  SC("Do not use non-ASCII for the translation of this item")
7762  fl_message_title(S("Error"));
7763  fl_alert("%s", S("Invalid URI format"));
7764  }
7765  else
7766  {
7767  clen = std::strlen(&uri[len]);
7768  ti = searchSelectArticle(&uri[len], clen);
7769  if(NULL == ti)
7770  {
7771  // Not found in article tree
7772  if(main_debug)
7773  {
7774  PRINT_ERROR("Try to fetch article from URI");
7775  }
7776  viewArticle(UI_CB_START, (const char*) &uri[len]);
7777  }
7778  }
7779  }
7780  }
7781  else
7782  {
7783  // Check for numeric value (Hyperlink to references list)
7784  clen = std::strlen(uri);
7785  if(std::strspn(uri, "0123456789") == clen)
7786  {
7787  //fl_message("%s", "Hyperlink to references list");
7788  if((std::size_t) INT_MAX >= clen && (std::size_t) 20 >= clen)
7789  {
7790  rv = enc_convert_ascii_to_anum(&ref_i, uri, (int) clen);
7791  if(!rv)
7792  {
7793  p = currentArticleHE->header->refs[ref_i];
7794  clen = std::strlen(p);
7795  q = new char[clen];
7796  std::strncpy(q, &p[1], clen - (std::size_t) 2);
7797  q[clen - (std::size_t) 2] = 0;
7798  viewArticle(UI_CB_START, q);
7799  delete[] q;
7800  }
7801  }
7802  }
7803  else
7804  {
7805  // Check for "mailto:" URI
7806  len = std::strlen(mailto);
7807  if(std::strlen(uri) >= len && !std::strncmp(uri, mailto, len))
7808  {
7809  if(std::strlen(uri) == len)
7810  {
7811  SC("Do not use non-ASCII for the translation of this item")
7812  fl_message_title(S("Error"));
7813  fl_alert("%s", S("Invalid URI format"));
7814  }
7815  else
7816  {
7817  // Yes => Use external e-mail handler
7818  // Note:
7819  // The 'mailto:' must be left in place to tell the
7820  // handler that this is an URI and not an unencoded
7821  // 'addr-spec'.
7822  rv = ext_handler_email(uri, NULL, NULL);
7823  }
7824  }
7825  else
7826  {
7827  // Check for "file:" URI
7828  len = std::strlen(file);
7829  if(std::strlen(uri) >= len
7830  && !std::strncmp(uri, file, len))
7831  {
7832  if(std::strlen(uri) == len)
7833  {
7834  SC("Do not use non-ASCII for the translation of this item")
7835  fl_message_title(S("Error"));
7836  fl_alert("%s", S("Invalid URI format"));
7837  }
7838  else
7839  {
7840  // Yes => Store MIME entity to file
7841  storeMIMEEntityToFile(uri,
7842  currentArticleHE->header->msgid);
7843  }
7844  }
7845  else
7846  {
7847  // Use external URI handler for all other clickable
7848  // links
7849  rv = ext_handler_uri(uri, &invalid);
7850  if(0 > rv && invalid)
7851  {
7852  SC("Do not use non-ASCII for the translation of this item")
7853  fl_message_title(S("Error"));
7854  fl_alert("%s", S("Invalid characters in URI"));
7855  }
7856  else if(rv)
7857  {
7858  SC("Do not use non-ASCII for the translation of this item")
7859  fl_message_title(S("Error"));
7860  fl_alert("%s", S("Starting external URI handler failed"));
7861  }
7862  }
7863  }
7864  }
7865  }
7866  }
7867  }
7868  }
7869  std::free((void*) uri);
7870 }
7871 
7872 
7873 // =============================================================================
7874 // Main window constructor
7875 //
7876 // \attention
7877 // The X display is not set yet when the main window is constructed. Therefore
7878 // anything done here is not allowed to trigger the connection to the X server.
7879 //
7880 // \param[in] label Window title
7881 
7882 MainWindow::MainWindow(const char* label) :
7883  UI_WINDOW_CLASS(730, 395, label)
7884 {
7885  const char* enabled = S("Enabled");
7886  const char* disabled = S("Disabled");
7887  const char* available = S("Available");
7888  const char* notavailable = S("Not available");
7889  Fl_Color signature = conf_integer_check(CONF_COLOR_SIGNATURE, 0x00, 0xFF);
7890  Fl_Color external = conf_integer_check(CONF_COLOR_EXTERNAL, 0x00, 0xFF);
7891  Fl_Color level1 = conf_integer_check(CONF_COLOR_LEVEL1, 0x00, 0xFF);
7892  Fl_Color level2 = conf_integer_check(CONF_COLOR_LEVEL2, 0x00, 0xFF);
7893  Fl_Color level3 = conf_integer_check(CONF_COLOR_LEVEL3, 0x00, 0xFF);
7894  Fl_Color level4 = conf_integer_check(CONF_COLOR_LEVEL4, 0x00, 0xFF);
7895  const Fl_Color styles_colors[UI_STYLES_LEN] =
7896  {
7897  FL_FOREGROUND_COLOR, // Plain bold
7898  signature, // Signature
7899  external, // External citation
7900  FL_BLUE, // Hyperlink (never change this color!)
7901  // -----------------------------------------------------------------------
7902  // Keep the following group continuous
7903  FL_FOREGROUND_COLOR, // Content
7904  level1, // Thread citation level 1 ("> ")
7905  level2, // Thread citation level 2 ("> > ")
7906  level3, // Thread citation level 3 ("> > > ")
7907  level4 // Thread citation level 4 ("> > > > ")
7908  };
7909  Fl_Group* mainGroup;
7910  Fl_Group* statusGroup;
7911  Fl_Tree_Item* ti;
7912  int i;
7913  char* p;
7914 
7915  mainState = STATE_READY;
7916  startup = true;
7917  busy = false;
7918  unsub = false;
7919  wrapMode = Fl_Text_Display::WRAP_NONE;
7920  hyperlinkStyle = 0;
7921  hyperlinkPosition = -1;
7922  rot13 = false;
7923 
7924  // Init group list, current group and current article
7925  group_num = 0;
7926  group_list = NULL;
7927  group_list_index = 0;
7928  group_old = 0; // No group selected
7929  group_new = 0;
7930  subscribedGroups = NULL;
7931  currentGroup = NULL;
7932  currentArticle = NULL;
7933  currentStyle = NULL;
7934  lastArticleHE = NULL;
7935  currentArticleHE = NULL;
7936  currentLine = 0;
7937 
7938  // Init current search state
7939  p = new char[1]; p[0] = 0; // Empty string
7940  currentSearchString = p;
7941  currentSearchPosition = 0;
7942 
7943  // Init progress bar state
7944  progress_skip_update = false;
7945 
7946  // Init MIME object pointer
7947  mimeData = NULL;
7948 
7949  // Init subescribecompose window pointer
7950  subscribeWindow = NULL;
7951 
7952  // Init compose window pointer and locking (for external editor)
7953  composeWindow = NULL;
7954  composeWindowLock = 0;
7955 
7956  // Install main window close callback
7957  callback(exit_cb, (void*) this);
7958 
7959  // Always dummy use both strings to avoid "unused" warnings from optimizer
7960  aboutString << enabled << disabled << available << notavailable;
7961  aboutString.str(std::string());
7962  aboutString.clear();
7963 
7964  // Create about message
7965  aboutString << CFG_NAME << " " << CFG_VERSION
7966  << " " << S("for") << " " << CFG_OS << "\n"
7967 #if defined(CFG_MODIFIED) && CFG_MODIFIED != 0
7968  // Don't use NLS for this string because of the license terms
7969  << "(This is a modified version!)" << "\n"
7970 #endif // CFG_MODIFIED
7971  // --------------------------------------------------------------
7972  << S("Unicode version") << ": " << UC_VERSION << "\n"
7973  // --------------------------------------------------------------
7974  << "NLS: "
7975 #if CFG_USE_XSI && !CFG_NLS_DISABLE
7976  << S("Enabled") << "\n"
7977 #else // CFG_USE_XSI && !CFG_NLS_DISABLE
7978  << S("Disabled") << "\n"
7979 #endif // CFG_USE_XSI && !CFG_NLS_DISABLE
7980  // --------------------------------------------------------------
7981  "TLS: "
7982 #if CFG_USE_TLS
7983  << S("Available") << "\n"
7984  << S("Required OpenSSL ABI") << ": "
7985 # if CFG_USE_LIBRESSL
7986  << "LibreSSL" << "\n"
7987 # else // CFG_USE_LIBRESSL
7988 # if !CFG_USE_OPENSSL_API_3 && !CFG_USE_OPENSSL_API_1_1
7989  << "1.0" << "\n"
7990 # else // !CFG_USE_OPENSSL_API_3 && !CFG_USE_OPENSSL_API_1_1
7991 # if !CFG_USE_OPENSSL_API_3
7992  << "1.1" << "\n"
7993 # else // !CFG_USE_OPENSSL_API_3
7994  << "3" << "\n"
7995 # endif // !CFG_USE_OPENSSL_API_3
7996 # endif // !CFG_USE_OPENSSL_API_3 && !CFG_USE_OPENSSL_API_1_1
7997 # endif // CFG_USE_LIBRESSL
7998 #else // CFG_USE_TLS
7999  << S("Not available") << "\n"
8000 #endif // CFG_USE_TLS
8001  // --------------------------------------------------------------
8002  << S("Required FLTK ABI") << ": " << FL_MAJOR_VERSION << "."
8003  << FL_MINOR_VERSION
8004 #ifdef FL_ABI_VERSION
8005  << "." << FL_ABI_VERSION % 100
8006 #else // FL_ABI_VERSION
8007  << "." << 0 // Not supported by FLTK 1.3.0
8008 #endif // FL_ABI_VERSION
8009  << "\n"
8010  // --------------------------------------------------------------
8011  << S("FLTK Double Buffering: ")
8012 #if CFG_DB_DISABLE
8013  << S("Disabled") << "\n"
8014 #else // CFG_DB_DISABLE
8015  << S("Enabled") << "\n"
8016 #endif // CFG_DB_DISABLE
8017  // --------------------------------------------------------------
8018  << S("Compression: ")
8019 #if CFG_CMPR_DISABLE
8020  << S("Disabled") << "\n"
8021 #else // CFG_CMPR_DISABLE
8022  << S("Available")
8023 # if CFG_USE_ZLIB
8024  << " (zlib)"
8025 # endif // CFG_USE_ZLIB
8026  << "\n"
8027 #endif // CFG_CMPR_DISABLE
8028  // --------------------------------------------------------------
8029  << "Build: " << BDATE << std::flush;
8030 
8031  // Make main window resizable
8032  resizable(this);
8033 
8034  // Add widgets --------------------------------------------------------------
8035  begin();
8036 
8037  // Main group
8038  mainGroup = new Fl_Group(0, 0, 730, 395);
8039  mainGroup->begin();
8040  {
8041  // Menu bar
8042 #if CFG_COCOA_SYS_MENUBAR
8043  menu = new Fl_Sys_Menu_Bar(0, 0, 730, 0);
8044 #else // CFG_COCOA_SYS_MENUBAR
8045  menu = new Fl_Menu_Bar(0, 0, 730, 30);
8046 #endif // CFG_COCOA_SYS_MENUBAR
8047  menu->selection_color(UI_COLOR_MENU_SELECTION);
8048 #if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
8049  // Recent FLTK 1.4 no longer shows selected entry with FL_FLAT_BOX
8050  menu->box(FL_THIN_UP_BOX);
8051 #else // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
8052  menu->box(FL_FLAT_BOX);
8053 #endif // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
8054  // --------------------
8055  SC("")
8056  SC("This section is for the main window menubar and pull-down menus.")
8057  SC("The 2 spaces after every string are intended and must be preserved.")
8058  SC("The part before the slash is the menubar entry and must be unique.")
8059  SC("The character after & is the key to pull-down a menu with 'Alt-key'.")
8060  SC("You can assign the & to any character before the slash,")
8061  SC("provided again that it is unique in the menubar!")
8062  SC("The part behind the slash is the name of the pull-down menu entry,")
8063  SC("it must only be unique inside the corresponding pull-down menu.")
8064  SC("If the name contains a slash, a submenu is created.")
8065  SC("Note:")
8066  SC("It is not possible to localize the keyboard shortcuts for the")
8067  SC("pull-down menu entries. It was implemented this way for easier")
8068  SC("documentation and for the possibility to execute blind keyboard")
8069  SC("commands in the case of a misconfigured locale.")
8070  menu->add(S("&File/Save article "), 0, asave_cb, (void*) this);
8071  menu->add(S("&File/Print article "), 0, print_cb, (void*) this);
8072  menu->add(S("&File/Quit "), "^q", exit_cb, (void*) this);
8073  // --------------------
8074  menu->add(S("&Edit/Server "), 0, server_cb, (void*) this);
8075  menu->add(S("&Edit/Configuration "), 0, config_cb, (void*) this);
8076  menu->add(S("&Edit/Identity "), 0, identity_cb, (void*) this);
8077  // --------------------
8078  i = FL_MENU_TOGGLE;
8079  if(config[CONF_TVIEW].val.i) { i |= FL_MENU_VALUE; }
8080  menu->add(S("&Group/Threaded view "), "^t", thrv_cb, (void*) this, i);
8081  i = FL_MENU_TOGGLE;
8082  if(config[CONF_TVIEW].val.i) { i |= FL_MENU_INACTIVE; }
8083  if(config[CONF_UTVIEW_AN].val.i) { i |= FL_MENU_VALUE; }
8084  menu->add(S("&Group/Sort by article number "), 0, uthrv_sort_cb,
8085  (void*) this, i);
8086  i = FL_MENU_TOGGLE;
8087  if(config[CONF_ONLYUR].val.i) { i |= FL_MENU_VALUE; }
8088  menu->add(S("&Group/Show only unread articles "), 0, onlyur_cb,
8089  (void*) this, i);
8090  menu->add(S("&Group/Subscribe "), 0, gsubscribe_cb, (void*) this);
8091  menu->add(S("&Group/Unsubscribe "), 0, gunsubscribe_cb, (void*) this);
8092  menu->add(S("&Group/Sort list "), 0, gsort_cb, (void*) this);
8093  menu->add(S("&Group/Refresh list "), "^r", grefresh_cb, (void*) this);
8094  menu->add(S("&Group/Next unread group "), "^g", nug_cb, (void*) this);
8095  menu->add(S("&Group/Mark subthread read "), 0, mssar_cb,
8096  (void*) this);
8097  menu->add(S("&Group/Mark all in group read "), "^a", maar_cb,
8098  (void*) this);
8099  menu->add(S("&Group/Mark all groups read "), 0, magar_cb,
8100  (void*) this);
8101  // --------------------
8102  menu->add(S("&Article/Search in article "), "/", asearch_cb, (void*) this);
8103  menu->add(S("&Article/Post to newsgroup "), "^p", compose_cb,
8104  (void*) this);
8105  menu->add(S("&Article/Followup to newsgroup "), "^f", reply_cb,
8106  (void*) this);
8107  menu->add(S("&Article/Supersede in newsgroup "), 0, supersede_cb,
8108  (void*) this);
8109  menu->add(S("&Article/Cancel in newsgroup "), 0, cancel_cb,
8110  (void*) this);
8111  menu->add(S("&Article/Reply by e-mail "), "^m", email_cb, (void*) this);
8112  menu->add(S("&Article/Wrap to width "), "^w", wrap_cb, (void*) this);
8113  menu->add(S("&Article/ROT13 "), "^o", rot13_cb, (void*) this);
8114  menu->add(S("&Article/Next unread article "), "^n", nua_cb,
8115  (void*) this);
8116  menu->add(S("&Article/Previous read article "), "^b", pra_cb,
8117  (void*) this);
8118  menu->add(S("&Article/View source "), "^e", viewsrc_cb, (void*) this);
8119  menu->add(S("&Article/Toggle read unread "), "^u", msau_cb,
8120  (void*) this);
8121  // --------------------
8122  i = FL_MENU_TOGGLE;
8123  if(main_debug) { i |= FL_MENU_VALUE; }
8124  menu->add(S("&Tools/Protocol console "), 0, console_cb, (void*) this);
8125  menu->add(S("&Tools/Search Message-ID "), "^s", mid_search_cb,
8126  (void*) this);
8127  // --------------------
8128  menu->add(S("&Help/About "), 0, about_cb, (void*) this);
8129  menu->add(S("&Help/Message of the day "), 0, viewmotd_cb, (void*) this);
8130  menu->add(S("&Help/License "), 0, license_cb, (void*) this);
8131  menu->add(S("&Help/Bug report "), 0, bug_cb, (void*) this);
8132  SC("")
8133  // --------------------
8134 
8135  // Content group
8136 #if CFG_COCOA_SYS_MENUBAR
8137  contentGroup = new Fl_Tile(0, 0, 730, 370);
8138 #else // CFG_COCOA_SYS_MENUBAR
8139  contentGroup = new Fl_Tile(0, 30, 730, 340);
8140 #endif // CFG_COCOA_SYS_MENUBAR
8141  contentGroup->begin();
8142  {
8143  // Group list
8144 #if CFG_COCOA_SYS_MENUBAR
8145  groupList = new Fl_Hold_Browser(0, 0, 230, 370);
8146 #else // CFG_COCOA_SYS_MENUBAR
8147  groupList = new Fl_Hold_Browser(0, 30, 230, 340);
8148 #endif // CFG_COCOA_SYS_MENUBAR
8149  groupList->callback(gselect_cb, (void*) this);
8150 #if CFG_COCOA_SYS_MENUBAR
8151  contentGroup2 = new Fl_Tile(230, 0, 500, 370);
8152 #else // CFG_COCOA_SYS_MENUBAR
8153  contentGroup2 = new Fl_Tile(230, 30, 500, 340);
8154 #endif // CFG_COCOA_SYS_MENUBAR
8155  contentGroup2->begin();
8156  {
8157  // Article tree
8158 #if CFG_COCOA_SYS_MENUBAR
8159  articleTree = new My_Tree(230, 0, 500, 140);
8160 #else // CFG_COCOA_SYS_MENUBAR
8161  articleTree = new My_Tree(230, 30, 500, 140);
8162 #endif // CFG_COCOA_SYS_MENUBAR
8163  articleTree->showroot(0);
8164  articleTree->connectorstyle(FL_TREE_CONNECTOR_SOLID);
8165  articleTree->connectorcolor(FL_FOREGROUND_COLOR);
8166  // Explicitly set foreground color to FLTK default
8167  // (default is not the same as for the other widgets)
8168  articleTree->item_labelfgcolor(FL_FOREGROUND_COLOR);
8169  ti = articleTree->add(S("No articles"));
8170  if(NULL != ti) { ti->deactivate(); }
8171  articleTree->callback(aselect_cb, (void*) this);
8172  // Article content window
8173 #if CFG_COCOA_SYS_MENUBAR
8174  text = new My_Text_Display(230, 140, 500, 230);
8175 #else // CFG_COCOA_SYS_MENUBAR
8176  text = new My_Text_Display(230, 170, 500, 200);
8177 #endif // CFG_COCOA_SYS_MENUBAR
8178  text->callback(hyperlink_cb, (void*) this);
8179  text->textfont(FL_COURIER);
8180  }
8181  contentGroup2->end();
8182  }
8183  contentGroup->end();
8184 
8185  // Status group
8186  statusGroup = new Fl_Group(0, 370, 730, 25);
8187  statusGroup->begin();
8188  {
8189  // Progress bar
8190  progressBar = new Fl_Progress(0, 370, 100, 25);
8191  progressBar->color(FL_BACKGROUND_COLOR, UI_COLOR_PROGRESS_BAR);
8192  progressBar->minimum(0.0);
8193  progressBar->maximum(100.0);
8194  progressBar->value(0.0);
8195  progressBar->label("");
8196  // Status bar
8197  statusBar = new Fl_Box(100, 370, 730, 25);
8198  statusBar->box(FL_DOWN_BOX);
8199  statusBar->align(FL_ALIGN_INSIDE | FL_ALIGN_LEFT);
8200  statusBar->label("");
8201  }
8202  statusGroup->end();
8203  statusGroup->resizable(statusBar);
8204  }
8205  mainGroup->end();
8206  mainGroup->resizable(contentGroup);
8207 
8208  end();
8209  // --------------------------------------------------------------------------
8210 
8211  // Allocate and init styles
8212  styles_len = UI_STYLES_LEN;
8213  styles = new Fl_Text_Display::Style_Table_Entry[styles_len];
8214  styles[0].color = FL_FOREGROUND_COLOR;
8215  styles[0].font = FL_COURIER_BOLD;
8216  styles[0].size = text->textsize();
8217  styles[0].attr = 0;
8218  for(i = 1; i < styles_len; ++i)
8219  {
8220  styles[i].color = styles_colors[i];
8221  styles[i].font = text->textfont();
8222  styles[i].size = text->textsize();
8223  styles[i].attr = 0;
8224  }
8225 }
8226 
8227 
8228 // =============================================================================
8229 // Main window destructor
8230 //
8231 // \attention
8232 // The FLTK documentation specifies that 'highlight_data()' can remove a style
8233 // buffer, but it's unclear how this should work. Passing 'NULL' is not allowed
8234 // because the pointer to the new style buffer is always dereferenced.
8235 // For 'buffer()' it is not explicitly allowed to pass 'NULL' in the
8236 // documentation too. Even if it works this may change in future releases.
8237 // As a workaround we assign a dummy text buffer before destroying the attached
8238 // one.
8239 
8240 MainWindow::~MainWindow(void)
8241 {
8242  if(NULL != subscribedGroups)
8243  {
8244  core_destroy_subscribed_group_info(&subscribedGroups);
8245  }
8246  if(NULL != mimeData) { delete mimeData; }
8247  delete[] currentSearchString;
8248  core_free(currentGroup);
8249  core_destroy_subscribed_group_states(&group_num, &group_list);
8250  // Detach highlight data from 'text' before destroying it
8251  text->highlight_data(dummyTb, NULL, 0, 'A', NULL, NULL);
8252  if(currentStyle) { delete currentStyle; }
8253  delete[] styles;
8254  // Detach current article from 'text' before destroying it
8255  text->buffer(dummyTb);
8256  if(currentArticle) { delete currentArticle; }
8257 }
8258 
8259 
8260 // =============================================================================
8261 // Server configuration window UI driving
8262 
8263 int ServerCfgWindow::process(void)
8264 {
8265  // Drive UI until user press OK or Cancel button
8266  while(!finished) { Fl::wait(); }
8267 
8268  // Copy data back to server configuration object
8269  sconf->serverReplace(scfgHostname->value());
8270  sconf->serviceReplace(scfgService->value());
8271  if(scfgTlsStrong->value()) { sconf->enc = UI_ENC_STRONG; }
8272  else if(scfgTlsWeak->value()) { sconf->enc = UI_ENC_WEAK; }
8273  else { sconf->enc = UI_ENC_NONE; }
8274  if(scfgAuthUser->value()) { sconf->auth = UI_AUTH_USER; }
8275  else { sconf->auth = UI_AUTH_NONE; }
8276 
8277  return(finished);
8278 }
8279 
8280 
8281 // =============================================================================
8282 // Server configuration window constructor
8283 
8284 ServerCfgWindow::ServerCfgWindow(ServerConfig* sc, const char* label) :
8285  UI_WINDOW_CLASS(700, 395, label)
8286 {
8287  Fl_Group* gp;
8288  Fl_Box* bp;
8289  Fl_Button* p;
8290  int y, w, h, gy, gh, bw;
8291  int w1, w2, h1, h2;
8292  std::ostringstream serverString;
8293  std::ostringstream serviceString;
8294  std::ostringstream tlsString;
8295  Fl_Group* grpTls;
8296 
8297  // Configure server window
8298  sconf = sc;
8299  finished = 0;
8300  copy_label(label);
8301  set_modal();
8302  callback(cancel_cb, (void*) this);
8303 
8304  // Prepare label for server hostname input field
8305  serverString << S("NNTP server hostname")
8306  << " (" << S("or IP address") << ")" << std::flush;
8307  // Prepare label for server service input field
8308  serviceString << S("Service name")
8309  << " (" << S("or TCP port") << ")" << std::flush;
8310  tlsString << "Transport Layer Security (TLS)"
8311  << " " << S("and server to client authentication") << std::flush;
8312 
8313  // Attention:
8314  //
8315  // const char* server = serverString.str().c_str()
8316  //
8317  // creates 'server' with undefined value because the compiler is
8318  // allowed to free the temporary string object returned by 'str()'
8319  // immediately after the assignment!
8320  // Assigning names to temporary string objects forces them to stay in
8321  // memory as long as their names go out of scope (this is what we
8322  // need).
8323  const std::string& srv_s = serverString.str();
8324  const std::string& svc_s = serviceString.str();
8325  const std::string& tls_s = tlsString.str();
8326 
8327  // Copy C strings
8328  hostname = new char[std::strlen(srv_s.c_str()) + (std::size_t) 1];
8329  std::strcpy(hostname, srv_s.c_str());
8330  service = new char[std::strlen(svc_s.c_str()) + (std::size_t) 1];
8331  std::strcpy(service, svc_s.c_str());
8332  tls_headline = new char[std::strlen(tls_s.c_str()) + (std::size_t) 1];
8333  std::strcpy(tls_headline, tls_s.c_str());
8334 
8335  // Create widget group
8336  scfgGroup = new Fl_Group(0, 0, 700, 395);
8337  scfgGroup->begin();
8338  {
8339  // Calculate required widths and heights
8340  gui_set_default_font();
8341  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
8342  fl_measure(S("OK"), w2 = 0, h2 = 0);
8343  if(w1 > w2) { w = w1; } else { w = w2; }
8344  if(h1 > h2) { h = h1; } else { h = h2; }
8345  w += 30;
8346  h += 10;
8347  gh = h + 10;
8348  gy = 395 - gh;
8349  // Add server hostname field
8350  scfgHostname = new Fl_Input(15, 35, 450, 30, hostname);
8351  scfgHostname->align(FL_ALIGN_TOP_LEFT);
8352  scfgHostname->value(sc->server);
8353  // Add server TCP port field
8354  scfgService = new Fl_Input(480, 35, 205, 30, service);
8355  scfgService->align(FL_ALIGN_TOP_LEFT);
8356  scfgService->value(sc->service);
8357  // Add TLS radio buttons
8358  grpTls = new Fl_Group(15, 100, 670, 120, tls_headline);
8359  grpTls->begin();
8360  {
8361  scfgTlsOff = new Fl_Radio_Round_Button(30, 115, 640, 30,
8362  S("Disabled"));
8363  scfgTlsOff->selection_color(UI_COLOR_RADIO_BUTTON);
8364  scfgTlsOff->callback(enc_off_cb, (void*) this);
8365  scfgTlsStrong = new Fl_Radio_Round_Button(30, 145, 640, 30,
8366  S("Enabled with strong encryption and forward secrecy"));
8367  scfgTlsStrong->selection_color(UI_COLOR_RADIO_BUTTON);
8368  scfgTlsStrong->callback(enc_on_cb, (void*) this);
8369  scfgTlsWeak = new Fl_Radio_Round_Button(30, 175, 640, 30,
8370  S("Enabled in compatibility mode offering weak cipher suites"));
8371  scfgTlsWeak->selection_color(UI_COLOR_RADIO_BUTTON);
8372  scfgTlsWeak->callback(enc_on_cb, (void*) this);
8373  }
8374  grpTls->end();
8375  grpTls->align(FL_ALIGN_TOP_LEFT);
8376  grpTls->box(FL_EMBOSSED_BOX);
8377 #if !CFG_USE_TLS
8378  grpTls->deactivate();
8379  sc->enc = UI_ENC_NONE;
8380 #endif // CFG_USE_TLS
8381  switch(sc->enc)
8382  {
8383  case UI_ENC_STRONG: { scfgTlsStrong->set(); break; }
8384  case UI_ENC_WEAK: { scfgTlsWeak->set(); break; }
8385  default: { scfgTlsOff->set(); break; }
8386  }
8387  // Add authentication radio buttons
8388  grpAuth = new Fl_Group(15, 255, 670, 90,
8389  S("Client to server authentication"));
8390  grpAuth->begin();
8391  {
8392  scfgAuthOff = new Fl_Radio_Round_Button(30, 270, 640, 30,
8393  S("Disabled"));
8394  scfgAuthOff->selection_color(UI_COLOR_RADIO_BUTTON);
8395  scfgAuthUser = new Fl_Radio_Round_Button(30, 300, 640, 30,
8396  S("AUTHINFO USER/PASS as defined in RFC 4643"));
8397  scfgAuthUser->selection_color(UI_COLOR_RADIO_BUTTON);
8398  }
8399  grpAuth->end();
8400  grpAuth->align(FL_ALIGN_TOP_LEFT);
8401  grpAuth->box(FL_EMBOSSED_BOX);
8402 #if !CFG_NNTP_AUTH_UNENCRYPTED
8403  if(UI_ENC_NONE == sc->enc)
8404  {
8405  scfgAuthOff->set();
8406  grpAuth->deactivate();
8407  }
8408  else
8409 #endif
8410  {
8411  switch(sc->auth)
8412  {
8413  case UI_AUTH_USER: { scfgAuthUser->set(); break; }
8414  default: { scfgAuthOff->set(); break; }
8415  }
8416  }
8417  // Add button bar at bottom
8418  gp = new Fl_Group(0, gy, 700, gh);
8419  gp->begin();
8420  {
8421  bw = w + 30;
8422  y = gy + 5;
8423  new Fl_Box(0, gy, bw, gh);
8424  p = new Fl_Button(15, y, w, h, S("OK"));
8425  p->callback(ok_cb, (void*) this);
8426  new Fl_Box(500 - bw, gy, bw, gh);
8427  p = new Fl_Button(700 - bw + 15, y, w, h, S("Cancel"));
8428  p->callback(cancel_cb, (void*) this);
8429  // Resizable space between buttons
8430  bp = new Fl_Box(bw, gy, 700 - (2 * bw), gh);
8431  }
8432  gp->end();
8433  gp->resizable(bp);
8434  }
8435  scfgGroup->end();
8436  resizable(scfgGroup);
8437  // Set fixed window size
8438  size_range(700, 395, 700, 395);
8439 #if !CFG_DB_DISABLE
8440  Fl::visual(FL_DOUBLE | FL_INDEX);
8441 #endif // CFG_DB_DISABLE
8442  show();
8443 }
8444 
8445 
8446 // =============================================================================
8447 // Server configuration window destructor
8448 
8449 ServerCfgWindow::~ServerCfgWindow(void)
8450 {
8451  delete[] hostname;
8452  delete[] service;
8453  delete[] tls_headline;
8454 }
8455 
8456 
8457 // =============================================================================
8458 // Identity configuration window callback
8459 //
8460 // Only store the Unicode data here, the encoding is done elsewhere.
8461 
8462 void IdentityCfgWindow::ok_cb_i(void)
8463 {
8464  std::size_t len1;
8465  std::size_t len2;
8466  char* buf;
8467 
8468  // Store "From" header in Unicode format (without MIME encoding)
8469  len1 = std::strlen(fromName->value());
8470  len2 = std::strlen(fromEmail->value());
8471  if(UI_HDR_BUFSIZE <= len1 || UI_HDR_BUFSIZE <= len2)
8472  {
8473  SC("Do not use non-ASCII for the translation of this item")
8474  fl_message_title(S("Error"));
8475  fl_alert("%s", S("From header field element too long"));
8476  }
8477  buf = new char[len1 + len2 + (std::size_t) 4];
8478  buf[0] = 0;
8479  if(len2)
8480  {
8481  if(len1) { std::strcat(buf, fromName->value()); }
8482  std::strcat(buf, " <");
8483  std::strcat(buf, fromEmail->value());
8484  std::strcat(buf, ">");
8485  }
8487  delete[] buf;
8488 
8489  // Store body for "Reply-To" header field
8490  len1 = std::strlen(replytoName->value());
8491  len2 = std::strlen(replytoEmail->value());
8492  if(UI_HDR_BUFSIZE <= len1 || UI_HDR_BUFSIZE <= len2)
8493  {
8494  SC("Do not use non-ASCII for the translation of this item")
8495  fl_message_title(S("Error"));
8496  fl_alert("%s", S("Reply-To header field element too long"));
8497  }
8498  buf = new char[len1 + len2 + (std::size_t) 4];
8499  buf[0] = 0;
8500  if(len2)
8501  {
8502  if(len1) { std::strcat(buf, replytoName->value()); }
8503  std::strcat(buf, " <");
8504  std::strcat(buf, replytoEmail->value());
8505  std::strcat(buf, ">");
8506  }
8507  if(std::strcmp(buf, config[CONF_FROM].val.s))
8508  {
8510  }
8511  else { conf_string_replace(&config[CONF_REPLYTO], ""); }
8512  delete[] buf;
8513 
8514  // Destroy configuration window
8515  Fl::delete_widget(this);
8516 }
8517 
8518 
8519 // =============================================================================
8520 // Identity configuration window constructor
8521 
8522 IdentityCfgWindow::IdentityCfgWindow(const char* label) :
8523  UI_WINDOW_CLASS(700, 200, label)
8524 {
8525  char from_name[UI_HDR_BUFSIZE + (std::size_t) 1];
8526  char from_email[UI_HDR_BUFSIZE + (std::size_t) 1];
8527  char replyto_name[UI_HDR_BUFSIZE + (std::size_t) 1];
8528  char replyto_email[UI_HDR_BUFSIZE + (std::size_t) 1];
8529  std::size_t i;
8530  Fl_Group* gp1;
8531  Fl_Group* gp2;
8532  Fl_Box* bp;
8533  Fl_Button* p;
8534  int y, w, h, gy, gh, bw;
8535  int w1, w2, h1, h2;
8536 
8537  // Configure identity window
8538  copy_label(label);
8539  set_modal();
8540  callback(cancel_cb, (void*) this);
8541 
8542  // Split current identity configuration into names and e-mail addresses
8543  std::strncpy(from_name, config[CONF_FROM].val.s, UI_HDR_BUFSIZE);
8544  from_name[UI_HDR_BUFSIZE] = 0;
8545  from_email[0] = 0;
8546  if(std::strlen(from_name))
8547  {
8548  i = 0;
8549  while(from_name[i])
8550  {
8551  if('<' == from_name[i])
8552  {
8553  // Attention: Overloaded and both prototypes different than in C!
8554  if(NULL == std::strchr(&from_name[i + (std::size_t) 1], (int) '<'))
8555  {
8556  // Remaining data doesn't contain another opening angle bracket
8557  if(!i || ((std::size_t) 1 == i && ' ' == from_name[0]))
8558  {
8559  from_name[0] = 0;
8560  }
8561  else { from_name[i - (std::size_t) 1] = 0; }
8562  std::strncpy(from_email, &from_name[++i], UI_HDR_BUFSIZE);
8563  i = std::strlen(from_email) - (std::size_t) 1;
8564  if('>' != from_email[i]) { from_email[0] = 0; }
8565  else { from_email[i] = 0; }
8566  break;
8567  }
8568  }
8569  ++i;
8570  }
8571  }
8572  std::strncpy(replyto_name, config[CONF_REPLYTO].val.s, UI_HDR_BUFSIZE);
8573  replyto_name[UI_HDR_BUFSIZE] = 0;
8574  replyto_email[0] = 0;
8575  if(std::strlen(replyto_name))
8576  {
8577  i = 0;
8578  while(replyto_name[i])
8579  {
8580  if('<' == replyto_name[i])
8581  {
8582  if(!i || ((std::size_t) 1 == i && ' ' == replyto_name[0]))
8583  {
8584  replyto_name[0] = 0;
8585  }
8586  else { replyto_name[i - (std::size_t) 1] = 0; }
8587  std::strncpy(replyto_email, &replyto_name[++i], UI_HDR_BUFSIZE);
8588  i = std::strlen(replyto_email) - (std::size_t) 1;
8589  if('>' != replyto_email[i]) { replyto_email[0] = 0; }
8590  else { replyto_email[i] = 0; }
8591  break;
8592  }
8593  else { ++i; }
8594  }
8595  }
8596 
8597  // Create widget group
8598  cfgGroup = new Fl_Group(0, 0, 700, 200);
8599  cfgGroup->begin();
8600  {
8601  // Calculate required widths and heights
8602  gui_set_default_font();
8603  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
8604  fl_measure(S("OK"), w2 = 0, h2 = 0);
8605  if(w1 > w2) { w = w1; } else { w = w2; }
8606  if(h1 > h2) { h = h1; } else { h = h2; }
8607  w += 30;
8608  h += 10;
8609  gh = h + 10;
8610  gy = 200 - gh;
8611 
8612  gp1 = new Fl_Group(0, 0, 700, gy);
8613  gp1->begin();
8614  {
8615  // Add "From" Name field
8616  fromName = new Fl_Input(15, 35, 325, 30, "From (Name):");
8617  fromName->align(FL_ALIGN_TOP_LEFT);
8618  fromName->value(from_name);
8619  // Add "From" e-mail field
8620  fromEmail = new Fl_Input(355, 35, 325, 30, "From (e-mail):");
8621  fromEmail->align(FL_ALIGN_TOP_LEFT);
8622  fromEmail->value(from_email);
8623  // Add "Reply-To" Name field
8624  replytoName = new Fl_Input(15, 100, 325, 30, "Reply-To (Name):");
8625  replytoName->align(FL_ALIGN_TOP_LEFT);
8626  replytoName->value(replyto_name);
8627  // Add "Reply-To" e-mail field
8628  replytoEmail = new Fl_Input(355, 100, 325, 30, "Reply-To (e-mail):");
8629  replytoEmail->align(FL_ALIGN_TOP_LEFT);
8630  replytoEmail->value(replyto_email);
8631  // Resizable space below input fields
8632  bp = new Fl_Box(0, 150, 700, gy - 150);
8633  //bp->box(FL_DOWN_BOX);
8634  }
8635  gp1->end();
8636  gp1->resizable(bp);
8637  // Add button bar at bottom
8638  gp2 = new Fl_Group(0, gy, 700, gh);
8639  gp2->begin();
8640  {
8641  bw = w + 30;
8642  y = gy + 5;
8643  new Fl_Box(0, gy, bw, gh);
8644  p = new Fl_Button(15, y, w, h, S("OK"));
8645  p->callback(ok_cb, (void*) this);
8646  new Fl_Box(500 - bw, gy, bw, gh);
8647  p = new Fl_Button(700 - bw + 15, y, w, h, S("Cancel"));
8648  p->callback(cancel_cb, (void*) this);
8649  // Resizable space between buttons
8650  bp = new Fl_Box(bw, gy, 700 - (2 * bw), gh);
8651  }
8652  gp2->end();
8653  gp2->resizable(bp);
8654  }
8655  cfgGroup->end();
8656  cfgGroup->resizable(gp1);
8657  resizable(cfgGroup);
8658  // Set minimum window size
8659  size_range(700, 150 + gh, 0, 0);
8660 #if !CFG_DB_DISABLE
8661  Fl::visual(FL_DOUBLE | FL_INDEX);
8662 #endif // CFG_DB_DISABLE
8663  show();
8664 }
8665 
8666 
8667 // =============================================================================
8668 // Identity configuration window destructor
8669 
8670 IdentityCfgWindow::~IdentityCfgWindow(void)
8671 {
8672 }
8673 
8674 
8675 // =============================================================================
8676 // Miscellaneous configuration window callback
8677 
8678 void MiscCfgWindow::ok_cb_i(void)
8679 {
8680  const char* is;
8681  const unsigned int cac_limit = (unsigned int) UI_CAC_MAX;
8682  unsigned int i;
8683  int error = 1;
8684 
8685  // Update CAC configuration
8686  is = cacField->value();
8687  if(NULL != is && 1 == std::sscanf(is, "%u", &i))
8688  {
8689  if(cac_limit < i) { i = cac_limit; }
8690  config[CONF_CAC].val.i = (int) i;
8691  error = 0;
8692  }
8693  else
8694  {
8695  SC("Do not use non-ASCII for the translation of this item")
8696  fl_message_title(S("Error"));
8697  fl_alert("%s", S("Invalid value for CAC"));
8698  }
8699 
8700  // Update checkbox settings
8701  if(!error)
8702  {
8703  if(cmprEnable->value()) { config[CONF_COMPRESSION].val.i = 1; }
8704  else { config[CONF_COMPRESSION].val.i = 0; }
8705  if(localTime->value()) { config[CONF_TS_LTIME].val.i = 1; }
8706  else { config[CONF_TS_LTIME].val.i = 0; }
8707  if(uagentEnable->value()) { config[CONF_ENABLE_UAGENT].val.i = 1; }
8708  else { config[CONF_ENABLE_UAGENT].val.i = 0; }
8709 
8710  if(qsSpace->value()) { config[CONF_QUOTESTYLE].val.i = 1; }
8711  else { config[CONF_QUOTESTYLE].val.i = 0; }
8712  if(qsUnify->value()) { config[CONF_QUOTEUNIFY].val.i = 1; }
8713  else { config[CONF_QUOTEUNIFY].val.i = 0; }
8714  }
8715 
8716  // Destroy misc configuration window if no error was detected
8717  if(!error) { Fl::delete_widget(this); }
8718 }
8719 
8720 
8721 // =============================================================================
8722 // Miscellaneous configuration window constructor
8723 
8724 MiscCfgWindow::MiscCfgWindow(const char* label) :
8725  UI_WINDOW_CLASS(400, 235, label)
8726 {
8727  Fl_Group* gp1;
8728  Fl_Group* gp2;
8729  Fl_Box* bp;
8730  Fl_Button* p;
8731  int th = 30; // Tab hight (including gap)
8732  int tg = 5; // Gap between tabs and cards
8733  int y, w, h, gy, gh, bw;
8734  int w1, w2, h1, h2;
8735  std::ostringstream ss;
8736  // Label strings
8737  SC("Preserve the spaces at beginning and end")
8738  const char* label_tab_cac = S(" Misc ");
8739  const char* label_tab_qs = S(" Quote style ");
8740  SC("Preserve the colon")
8741  const char* label_cac = S("Clamp article count threshold:");
8742 
8743  // Configure config window
8744  copy_label(label);
8745  set_modal();
8746  callback(cancel_cb, (void*) this);
8747 
8748  // Create widget group
8749  gp1 = new Fl_Group(0, 0, 400, 235);
8750  gp1->begin();
8751  {
8752  // Calculate required widths and heights
8753  gui_set_default_font();
8754  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
8755  fl_measure(S("OK"), w2 = 0, h2 = 0);
8756  if(w1 > w2) { w = w1; } else { w = w2; }
8757  if(h1 > h2) { h = h1; } else { h = h2; }
8758  w += 30;
8759  h += 10;
8760  gh = h + 10;
8761  gy = 235 - gh;
8762 
8763  cfgTabs = new Fl_Tabs(0, 0, 400, gy);
8764  cfgTabs->begin();
8765  {
8766  cacGroup = new Fl_Group(0, th - tg, 400, 235 - th + tg, label_tab_cac);
8767  cacGroup->begin();
8768  {
8769  cacField = new Fl_Input(15, 55, 400 - 30, 30, label_cac);
8770  cacField->align(FL_ALIGN_TOP_LEFT);
8771  // Note: We cannot use 'posix_snprintf()' here
8772  ss << config[CONF_CAC].val.i << std::flush;
8773  const std::string& s = ss.str();
8774  cacField->value(s.c_str());
8775  cmprEnable = new Fl_Check_Button(15, 90, 400 - 30, 30,
8776  S("Negotiate compression if available"));
8777  cmprEnable->tooltip(
8778  SC("This is the tooltip for the compression negotiation checkbox")
8779  S("Not recommended when confidential newsgroups are accessed")
8780  );
8781  cmprEnable->value(config[CONF_COMPRESSION].val.i);
8782 #if CFG_CMPR_DISABLE
8783  cmprEnable->deactivate();
8784 #endif // CFG_CMPR_DISABLE
8785  localTime = new Fl_Check_Button(15, 120, 400 - 30, 30,
8786  S("Use localtime for Date header field"));
8787  localTime->tooltip(
8788  SC("This is the tooltip for the use localtime checkbox")
8789  S("Using localtime is recommended by RFC 5322")
8790  );
8791  localTime->value(config[CONF_TS_LTIME].val.i);
8792  uagentEnable = new Fl_Check_Button(15, 150, 400 - 30, 30,
8793  S("Add User-Agent header field"));
8794  uagentEnable->value(config[CONF_ENABLE_UAGENT].val.i);
8795  // Resizable space below input fields
8796  bp = new Fl_Box(0, 180, 400, gy - 150);
8797  //bp->box(FL_DOWN_BOX);
8798  }
8799  cacGroup->end();
8800  cacGroup->resizable(bp);
8801 
8802  // --------------------------------------------------------------------
8803 
8804  qsGroup = new Fl_Group(0, th - tg, 400, 235 - th + tg, label_tab_qs);
8805  qsGroup->begin();
8806  {
8807  qsSpace = new Fl_Check_Button(15, 45, 400 - 30, 30,
8808  S("Space after quote marks"));
8809  qsSpace->value(config[CONF_QUOTESTYLE].val.i);
8810  qsUnify = new Fl_Check_Button(15, 75, 400 - 30, 30,
8811  S("Unify quote marks for follow-up"));
8812  qsUnify->value(config[CONF_QUOTEUNIFY].val.i);
8813  // Resizable space below input fields
8814  bp = new Fl_Box(0, 150, 400, gy - 150);
8815  //bp->box(FL_DOWN_BOX);
8816  }
8817  qsGroup->end();
8818  qsGroup->resizable(bp);
8819  }
8820  cfgTabs->end();
8821  cfgTabs->resizable(cacGroup);
8822  // Add button bar at bottom
8823  gp2 = new Fl_Group(0, gy, 400, gh);
8824  gp2->begin();
8825  {
8826  bw = w + 30;
8827  y = gy + 5;
8828  new Fl_Box(0, gy, bw, gh);
8829  p = new Fl_Button(15, y, w, h, S("OK"));
8830  p->callback(ok_cb, (void*) this);
8831  new Fl_Box(500 - bw, gy, bw, gh);
8832  p = new Fl_Button(400 - bw + 15, y, w, h, S("Cancel"));
8833  p->callback(cancel_cb, (void*) this);
8834  // Resizable space between buttons
8835  bp = new Fl_Box(bw, gy, 400 - (2 * bw), gh);
8836  }
8837  gp2->end();
8838  gp2->resizable(bp);
8839  }
8840  gp1->end();
8841  gp1->resizable(cfgTabs);
8842  resizable(gp1);
8843  // Set minimum window size
8844  size_range(400, 235, 0, 0);
8845 #if !CFG_DB_DISABLE
8846  Fl::visual(FL_DOUBLE | FL_INDEX);
8847 #endif // CFG_DB_DISABLE
8848  show();
8849 }
8850 
8851 
8852 // =============================================================================
8853 // Miscellaneous configuration window destructor
8854 
8855 MiscCfgWindow::~MiscCfgWindow(void)
8856 {
8857 }
8858 
8859 
8860 // =============================================================================
8861 // Search window callback
8862 
8863 void SearchWindow::ok_cb_i(void)
8864 {
8865  const char* ss;
8866  const char* ss_nfc;
8867  int len;
8868  std::size_t tmp;
8869  char* p;
8870 
8871  // Check search string
8872  ss = searchField->value();
8873  // Convert search string to Unicode Normalization Form C
8874  ss_nfc = enc_convert_to_utf8_nfc(ENC_CS_UTF_8, ss);
8875  if(NULL == ss_nfc)
8876  {
8877  UI_STATUS(S("Search failed."));
8878  SC("Do not use non-ASCII for the translation of this item")
8879  fl_message_title(S("Error"));
8880  fl_alert("%s", S("Processing Unicode data in search string failed"));
8881  }
8882  else
8883  {
8884  if(std::strcmp(*currentSearchString, ss_nfc))
8885  {
8886  // Search string has changed
8887  tmp = std::strlen(ss_nfc);
8888  if((std::size_t) INT_MAX <= tmp) { len = INT_MAX - 1; }
8889  else { len = (int) tmp; }
8890  // Replace current search string
8891  delete[] *currentSearchString;
8892  p = new char[len + 1];
8893  std::strncpy(p, ss_nfc, (size_t) len);
8894  p[len] = 0;
8895  *currentSearchString = p;
8896  }
8897  // Release memory for normalized string (if allocated)
8898  if(ss != ss_nfc) { enc_free((void*) ss_nfc); }
8899 
8900  // Check for case-insensitive search request
8901  config[CONF_SEARCH_CASE_IS].val.i = cisEnable->value();
8902  }
8903 }
8904 
8905 
8906 // =============================================================================
8907 // Search window constructor
8908 
8909 SearchWindow::SearchWindow(const char* label, const char** oldSearchString) :
8910  UI_WINDOW_CLASS(400, 165, label)
8911 {
8912  Fl_Group* gp; // Toplevel group in window
8913  Fl_Group* searchGroup;
8914  Fl_Group* searchGroup2;
8915  Fl_Group* buttonGroup;
8916  Fl_Box* bp;
8917  Fl_Button* p;
8918  int y, w, h, gy, gh, bw;
8919  int w1, w2, h1, h2;
8920  std::ostringstream ss;
8921  // Label strings
8922  SC("Preserve the colon")
8923  const char* label_search = S("Search for:");
8924 
8925  // Init finished flag
8926  finished = 0;
8927 
8928  // Init pointer to current search string
8929  currentSearchString = oldSearchString;
8930 
8931  // Configure config window
8932  copy_label(label);
8933  set_modal();
8934  callback(cancel_cb, (void*) this);
8935 
8936  // Create widget group
8937  gp = new Fl_Group(0, 0, 400, 165);
8938  gp->begin();
8939  {
8940  // Calculate required widths and heights
8941  gui_set_default_font();
8942  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
8943  fl_measure(S("OK"), w2 = 0, h2 = 0);
8944  if(w1 > w2) { w = w1; } else { w = w2; }
8945  if(h1 > h2) { h = h1; } else { h = h2; }
8946  w += 30;
8947  h += 10;
8948  gh = h + 10; // Height of bottom button bar
8949  gy = 165 - gh; // Height of top group
8950  searchGroup = new Fl_Group(0, 0, 400, 165 - gh);
8951  searchGroup->begin();
8952  {
8953  // Wrap a separate group around the input field
8954  searchGroup2 = new Fl_Group(0, 0, 400, 80);
8955  searchGroup2->begin();
8956  {
8957  // Input field in inner group
8958  searchField = new Fl_Input(15, 35, 400 - 30, 30, label_search);
8959  searchField->align(FL_ALIGN_TOP_LEFT);
8960  // Note: We cannot use 'posix_snprintf()' here
8961  ss << *currentSearchString << std::flush;
8962  const std::string& s = ss.str();
8963  searchField->value(s.c_str());
8964  searchField
8965 #if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
8966  // Requires FLTK 1.4.0
8967  ->insert_position
8968 #else // defined FL_ABI_VERSION && 10303 <= FL_ABI_VERSION
8969  ->position
8970 #endif // defined FL_ABI_VERSION && 10303 <= FL_ABI_VERSION
8971  (searchField->size(), 0);
8972  searchField->take_focus();
8973  }
8974  searchGroup2->end();
8975  // Check button in outer group
8976  cisEnable = new Fl_Check_Button(15, 80, 400 - 30, 30,
8977  S("Search case-insensitive"));
8978  cisEnable->value(config[CONF_SEARCH_CASE_IS].val.i);
8979  cisEnable->clear_visible_focus();
8980  // Resizable space below input fields
8981  bp = new Fl_Box(0, 110, 400, gy - 110);
8982  //bp->box(FL_DOWN_BOX);
8983  //bp->color(FL_GREEN);
8984  }
8985  searchGroup->end();
8986  searchGroup->resizable(bp);
8987  // Add button bar at bottom
8988  buttonGroup = new Fl_Group(0, gy, 400, gh);
8989  buttonGroup->begin();
8990  {
8991  bw = w + 30;
8992  y = gy + 5;
8993  new Fl_Box(0, gy, bw, gh);
8994  p = new Fl_Button(15, y, w, h, S("OK"));
8995  p->callback(ok_cb, (void*) this);
8996  p->shortcut(FL_Enter);
8997  p->clear_visible_focus();
8998  new Fl_Box(400 - bw, gy, bw, gh);
8999  p = new Fl_Button(400 - bw + 15, y, w, h, S("Cancel"));
9000  p->callback(cancel_cb, (void*) this);
9001  p->shortcut(FL_Escape);
9002  p->clear_visible_focus();
9003  // Resizable space between buttons
9004  bp = new Fl_Box(bw, gy, 400 - (2 * bw), gh);
9005  //bp->box(FL_DOWN_BOX);
9006  //bp->color(FL_YELLOW);
9007  }
9008  buttonGroup->end();
9009  buttonGroup->resizable(bp);
9010  }
9011  gp->end();
9012  gp->resizable(searchGroup);
9013  resizable(gp);
9014  // Set minimum window size
9015  size_range(400, 165, 0, 0);
9016 #if !CFG_DB_DISABLE
9017  Fl::visual(FL_DOUBLE | FL_INDEX);
9018 #endif // CFG_DB_DISABLE
9019  show();
9020 }
9021 
9022 
9023 // =============================================================================
9024 // Search window destructor
9025 
9026 SearchWindow::~SearchWindow(void)
9027 {
9028 }
9029 
9030 
9031 // =============================================================================
9032 // MIME content element decoder
9033 //
9034 // All the headerfields currently used are mandatory and the CORE module ensures
9035 // that strings are not NULL.
9036 // This must be checked here if optional header fields are used in the future!
9037 //
9038 // The caller is responsible to enc_free() the memory allocated for the result.
9039 
9040 const char* MIMEContent::createMessageHeader(const char* message,
9041  std::size_t len)
9042 {
9043  const char* res = NULL;
9044  core_article_header* hdr = NULL;
9045  std::size_t unused;
9046  Fl_Text_Buffer tb;
9047  char date[20];
9048  std::size_t i = 0;
9049  const char* p;
9050 
9051  // Split and parse header of message
9052  p = core_entity_parser(message, len, &hdr, &unused);
9053  if(NULL == p)
9054  {
9055  // Invalid message
9056  tb.append("[Invalid MIME message/rfc822 content]");
9057  tb.append("\n");
9058  }
9059  else
9060  {
9061  // From header field
9062  tb.append(S("From"));
9063  tb.append(": ");
9064  tb.append(hdr->from);
9065  tb.append("\n");
9066 
9067  // Subject header field
9068  tb.append(S("Subject"));
9069  tb.append(": ");
9070  tb.append(hdr->subject);
9071  tb.append("\n");
9072 
9073  // Date header field (a zero timestamp is treated as missing header field)
9074  tb.append(S("Date"));
9075  tb.append(": ");
9076  if(!hdr->date || enc_convert_posix_to_iso8601(date, hdr->date))
9077  {
9078  // No NLS support here to stay consistent with CORE
9079  tb.append("[Missing or invalid header field]");
9080  }
9081  else
9082  {
9083  tb.append(date);
9084  }
9085  tb.append("\n");
9086 
9087  // Message-ID header field
9088  tb.append(S("Message-ID"));
9089  tb.append(": ");
9090  if(!hdr->msgid[0])
9091  {
9092  // No NLS support here to stay consistent with CORE
9093  tb.append("[Missing or invalid header field]");
9094  }
9095  else
9096  {
9097  tb.append(hdr->msgid);
9098  }
9099  tb.append("\n");
9100 
9101  // Newsgroups header field
9102  tb.append(S("Newsgroups"));
9103  tb.append(": ");
9104  while(hdr->groups[i])
9105  {
9106  if(!i && !hdr->groups[0][0])
9107  {
9108  // No NLS support here to stay consistent with CORE
9109  tb.append("[Missing or invalid header field]");
9110  break;
9111  }
9112  else
9113  {
9114  if(i) { tb.append(","); }
9115  tb.append(hdr->groups[i++]);
9116  }
9117  }
9118  tb.append("\n");
9119  }
9120 
9121  // Copy result
9122  p = tb.text();
9124 
9125  std::free((void*) p);
9127 
9128  return(res);
9129 }
9130 
9131 
9132 // =============================================================================
9133 // MIME content element decoder
9134 //
9135 // \attention
9136 // This method must be reentrant (calls itself to recursively decode nested
9137 // multipart entities
9138 
9139 MIMEContent::MIMEContentListElement*
9140 MIMEContent::decodeElement(const char* p, std::size_t len, char* boundary,
9141  bool alt, bool digest, std::size_t* count)
9142 {
9143  MIMEContentListElement* res = NULL; // Result list
9144  MIMEContentListElement* cle = NULL; // Current list element(s)
9145  MIMEContentListElement* ble = NULL; // Buffered list element(s)
9146  // for multipart/alternative
9147  MIMEContentListElement* mle = NULL; // Encapsulated List element(s)
9148  // for message/rfc822
9149  MIMEContentListElement* lle = NULL; // Last list element
9150  std::size_t ae; // Additional elements
9151  std::size_t bae = 0; // Buffered additional elements
9152  std::size_t num = 0;
9153  enc_mime_mpe* mpe = NULL; // Multipart entities
9154  enc_mime_cte cte;
9155  enc_mime_ct ct;
9156  bool nested_multi;
9157  char nested_boundary[ENC_BO_BUFLEN];
9158  bool nested_alt;
9159  bool nested_digest;
9160  bool message_rfc822;
9161  core_article_header* e_h = NULL;
9162  std::size_t e_len;
9163  const char* q;
9164  std::size_t i;
9165  MIMEContentListElement* tmp;
9166 
9167  // Split multipart message into entities
9168  if(main_debug)
9169  {
9170  if(!std::strlen(boundary))
9171  {
9172  PRINT_ERROR("vvv Extract MIME encapsulated message vvv");
9173  }
9174  else
9175  {
9176  PRINT_ERROR("--- Split MIME multipart entity ---");
9177  }
9178  fprintf(stderr, "%s: %sSize of body: %lu octets\n",
9179  CFG_NAME, MAIN_ERR_PREFIX, (unsigned long int) len);
9180  }
9181  if(!std::strlen(boundary))
9182  {
9183  num = enc_mime_message(p, len, &mpe);
9184  }
9185  else
9186  {
9187  num = enc_mime_multipart(p, boundary, &mpe);
9188  }
9189  for(i = 0; i < num; ++i)
9190  {
9191  cte = ENC_CTE_BIN;
9192  nested_multi = false;
9193  nested_alt = false;
9194  nested_digest = false;
9195  message_rfc822 = false;
9196  ae = 0;
9197  // Split and parse header of entity
9198  q = core_entity_parser(mpe[i].start, mpe[i].len, &e_h, &e_len);
9199  if(NULL == q)
9200  {
9201  // Invalid entity => Handle whole entity as raw binary octet stream
9202  PRINT_ERROR("Invalid entity in MIME multipart entity");
9203  e_h = NULL;
9204  e_len = mpe[i].len;
9205  ct.type = ENC_CT_APPLICATION;
9207  ct.charset = ENC_CS_UNKNOWN;
9208  ct.flags = 0;
9209  }
9210  else
9211  {
9212  p = q;
9213  // Default to "text/plain; charset=US-ASCII" as defined by RFC 2046
9214  ct.type = ENC_CT_TEXT;
9215  ct.subtype = ENC_CTS_PLAIN;
9216  ct.charset = ENC_CS_ASCII;
9217  ct.flags = 0;
9218  // Use "message/rfc822" for multipart/digest as defined by RFC 2046
9219  if(digest)
9220  {
9221  ct.type = ENC_CT_MESSAGE;
9222  ct.subtype = ENC_CTS_RFC822;
9223  ct.charset = ENC_CS_UNKNOWN;
9224  ct.flags = 0;
9225  message_rfc822 = true;
9226  }
9227  // Check whether content type header field exist
9228  if(NULL == e_h->mime_ct)
9229  {
9230  if(main_debug)
9231  {
9232  if(!digest)
9233  {
9234  PRINT_ERROR("Content-Type: Text (using default)");
9235  }
9236  else
9237  {
9238  PRINT_ERROR("Content-Type: Message (using default)");
9239  }
9240  }
9241  }
9242  else
9243  {
9244  message_rfc822 = false;
9245  // Yes => Check whether content transfer encoding is supported
9246  cte = enc_mime_get_cte(e_h->mime_cte);
9247  if(ENC_CTE_UNKNOWN == cte)
9248  {
9249  PRINT_ERROR("Unknown MIME Content-Transfer-Encoding");
9250  // No => Treat as arbitrary binary data
9251  cte = ENC_CTE_BIN;
9252  // RFC 2049 requires that such content must be treated as type
9253  // "application/octet-stream".
9254  ct.type = ENC_CT_APPLICATION;
9256  ct.charset = ENC_CS_UNKNOWN;
9257  }
9258  else
9259  {
9260  // Yes => Check content type
9261  enc_mime_get_ct(&ct, e_h->mime_ct, nested_boundary);
9262  switch(ct.type)
9263  {
9264  case ENC_CT_TEXT:
9265  {
9266  if(main_debug)
9267  {
9268  PRINT_ERROR("Content-Type: Text");
9269  }
9270  // Check character set
9271  if(ENC_CS_UNKNOWN == ct.charset)
9272  {
9273  // Content character set not supported
9274  // RFC 2049 requires that such content must be treated
9275  // as type "application/octet-stream".
9276  ct.type = ENC_CT_APPLICATION;
9278  ct.charset = ENC_CS_UNKNOWN;
9279  }
9280  break;
9281  }
9282  case ENC_CT_IMAGE:
9283  case ENC_CT_AUDIO:
9284  case ENC_CT_VIDEO:
9285  {
9286  if(main_debug)
9287  {
9288  PRINT_ERROR("Content-Type: Image, Audio or Video");
9289  }
9290  ct.subtype = ENC_CTS_UNKNOWN;
9291  break;
9292  }
9293  case ENC_CT_MULTIPART:
9294  {
9295  if(main_debug)
9296  {
9297  PRINT_ERROR("Content-Type: Multipart (nested)");
9298  }
9299  nested_multi = true;
9300  if(ENC_CTS_ALTERNATIVE == ct.subtype)
9301  {
9302  nested_alt = true;
9303  }
9304  else if(ENC_CTS_DIGEST == ct.subtype)
9305  {
9306  nested_digest = true;
9307  }
9308  break;
9309  }
9310  case ENC_CT_MESSAGE:
9311  {
9312  if(main_debug)
9313  {
9314  PRINT_ERROR("Content-Type: Message");
9315  }
9316  if(ENC_CTS_RFC822 == ct.subtype)
9317  {
9318  message_rfc822 = true;
9319  }
9320  else
9321  {
9322  ct.type = ENC_CT_APPLICATION;
9324  ct.charset = ENC_CS_UNKNOWN;
9325  }
9326  break;
9327  }
9328  default:
9329  {
9330  if(main_debug)
9331  {
9332  PRINT_ERROR("Content-Type not supported");
9333  }
9334  // Handle anything unknown as type
9335  // "application/octet-stream"
9336  ct.type = ENC_CT_APPLICATION;
9338  ct.charset = ENC_CS_UNKNOWN;
9339  break;
9340  }
9341  }
9342  }
9343  }
9344  }
9345  // Special handling for MIME content type "multipart/alternative"
9346  if(alt)
9347  {
9348  // Check whether more sophisticated alternative is supported
9349  if(i)
9350  {
9351  if(!(ENC_CT_TEXT == ct.type && ENC_CTS_PLAIN == ct.subtype
9352  && ENC_CTE_UNKNOWN != cte))
9353  {
9354  // Ignore this alternative
9356  continue;
9357  }
9358  }
9359  }
9360  // Create new content element(s)
9361  if(nested_multi)
9362  {
9363  // Recursively decode nested multipart entities
9364  cle = decodeElement(p, e_len,
9365  nested_boundary, nested_alt, nested_digest, &ae);
9366  }
9367  // Create new element for encapsulated header of type message/rfc822
9368  else if(message_rfc822)
9369  {
9370  // Decode encapsulated entity
9371  nested_boundary[0] = 0;
9372  mle = decodeElement(p, e_len,
9373  nested_boundary, nested_alt, nested_digest, &ae);
9374  // Create additional list element for header of encapsulated message
9375  cte = ENC_CTE_8BIT;
9376  ct.type = ENC_CT_TEXT;
9377  ct.subtype = ENC_CTS_PLAIN;
9378  ct.charset = ENC_CS_UTF_8;
9379  ct.flags = 0;
9380  q = createMessageHeader(p, len);
9381  cle = initElement(q, std::strlen(q), cte, &ct, e_h);
9382  enc_free((void*) q);
9383  ++ae;
9384  // Append element for the message header first
9385  cle->next = mle;
9386  // Append additional list element for end of encapsulated message
9387  SC("Control characters for line break at the end must stay in place")
9388  q = S("End of encapsulated message.\r\n");
9389  mle = initElement(q, std::strlen(q), cte, &ct, e_h);
9390  ++ae;
9391  // Append element for EOM indication at the end
9392  tmp = cle;
9393  while(NULL != tmp->next) { tmp = tmp->next; }
9394  tmp->next = mle;
9395  }
9396  else
9397  {
9398  // Create new element
9399  cle = initElement(p, e_len, cte, &ct, e_h);
9400  ++ae;
9401  }
9402  if(NULL == cle)
9403  {
9404  PRINT_ERROR("Decoding MIME multipart message failed");
9405  break;
9406  }
9407  // Special handling for MIME content type "multipart/alternative"
9408  if(alt)
9409  {
9410  // Update current choice
9411  ble = cle;
9412  bae = ae;
9413  }
9414  else
9415  {
9416  // Append new element(s) to linked list
9417  if(!i) { res = cle; } else { lle->next = cle; }
9418  // Skip to last element
9419  lle = cle;
9420  while(NULL != lle->next) { lle = lle->next; }
9421  // Update element count
9422  *count += ae;
9423  }
9424  // End of loop
9426  }
9427  if(num) { enc_free((void*) mpe); }
9428  // Special handling for MIME content type "multipart/alternative"
9429  if(alt)
9430  {
9431  // Append only the last supported entity to linked list
9432  res = ble;
9433  // Update element count
9434  *count += bae;
9435  }
9436  // Debug message when multipart content or encapsulated message ends
9437  if(main_debug)
9438  {
9439  if(std::strlen(boundary))
9440  {
9441  PRINT_ERROR("--- End of MIME multipart entity ---");
9442  }
9443  else
9444  {
9445  PRINT_ERROR("^^^ End of MIME encapsulated message ^^^");
9446  }
9447  }
9448 
9449  return(res);
9450 }
9451 
9452 
9453 // =============================================================================
9454 // MIME content element constructor
9455 
9456 MIMEContent::MIMEContentListElement*
9457 MIMEContent::initElement(const char* body, std::size_t body_len,
9458  enc_mime_cte cte, enc_mime_ct* ct,
9459  struct core_article_header* hdr)
9460 {
9461  MIMEContentListElement* cle = new MIMEContentListElement;
9462  char* cp;
9463  Fl_Text_Buffer tb;
9464  bool headerPresent = false;
9465 
9466  // Content-Transfer-Encoding
9467  cle->cte = cte;
9468 
9469  // Content-Type
9470  std::memcpy((void*) &cle->ct, (void*) ct, sizeof(enc_mime_ct));
9471 
9472  // Body
9473  cp = new char[body_len + (std::size_t) 1];
9474  std::strncpy(cp, body, body_len); cp[body_len] = 0;
9475  cle->content = cp;
9476 
9477  // Entity headers for multipart content
9478  cle->type = ENC_CD_INLINE;
9479  cle->filename = NULL;
9480  cle->header = NULL;
9481  tb.text("");
9482  if(NULL != hdr && NULL != hdr->mime_cte)
9483  {
9484  tb.append(S("Transfer-Encoding"));
9485  tb.append(": ");
9486  tb.append(hdr->mime_cte);
9487  tb.append("\n");
9488  headerPresent = true;
9489  }
9490  if(NULL != hdr && NULL != hdr->mime_ct)
9491  {
9492  tb.append(S("Content-Type"));
9493  tb.append(": ");
9494  tb.append(hdr->mime_ct);
9495  tb.append("\n");
9496  headerPresent = true;
9497  }
9498  if(NULL != hdr && NULL != hdr->mime_cd)
9499  {
9500  tb.append(S("Content-Disposition"));
9501  tb.append(": ");
9502  tb.append(hdr->mime_cd);
9503  tb.append("\n");
9504  headerPresent = true;
9505  // Store type and filename for entity
9506  enc_mime_get_cd(hdr->mime_cd, &cle->type, &cle->filename);
9507  }
9508  if(headerPresent)
9509  {
9510  cle->header = tb.text();
9511  }
9512 
9513  // Terminate linked list
9514  cle->next = NULL;
9515 
9516  return(cle);
9517 }
9518 
9519 
9520 // =============================================================================
9521 // MIME content object constructor
9522 
9523 MIMEContent::MIMEContent(struct core_article_header* h, const char* p)
9524 {
9525  char boundary[ENC_BO_BUFLEN];
9526  enc_mime_cte cte = ENC_CTE_BIN;
9527  enc_mime_ct ct;
9528  bool multi;
9529  bool alt; // Multipart entity contains multiple variants of same content
9530  bool digest; // Default content type for multipart is message/rfc822
9531 
9532  // Init linked list
9533  partList = NULL;
9534  partNum = 0;
9535 
9536  // Check for MIME declaration (always assume version 1.0)
9537  if(NULL == h->mime_v)
9538  {
9539  if(NULL != h->mime_cte || NULL != h->mime_ct)
9540  {
9541  PRINT_ERROR("Trying to decode MIME article "
9542  "with missing MIME-Version header field");
9543  }
9544  }
9545  // Default to "text/plain; charset=US-ASCII" as defined by RFC 2045
9546  multi = false;
9547  alt = false;
9548  digest = false;
9549  ct.type = ENC_CT_TEXT;
9550  ct.subtype = ENC_CTS_PLAIN;
9551  ct.charset = ENC_CS_ASCII;
9552  ct.flags = 0;
9553  // Check whether content type header field exist
9554  if(NULL != h->mime_ct || NULL != h->mime_cte)
9555  {
9556  // Yes => Check whether content transfer encoding is supported
9557  cte = enc_mime_get_cte(h->mime_cte);
9558  if(ENC_CTE_UNKNOWN == cte)
9559  {
9560  // No => Treat as arbitrary binary data
9561  cte = ENC_CTE_BIN;
9562  // RFC 2049 requires that such content must be treated as type
9563  // "application/octet-stream".
9564  ct.type = ENC_CT_APPLICATION;
9566  ct.charset = ENC_CS_UNKNOWN;
9567  }
9568  else
9569  {
9570  // Yes => Check content type
9571  enc_mime_get_ct(&ct, h->mime_ct, boundary);
9572  switch(ct.type)
9573  {
9574  case ENC_CT_TEXT:
9575  {
9576  // Check character set
9577  if(ENC_CS_UNKNOWN == ct.charset)
9578  {
9579  // Content character set not supported
9580  // RFC 2049 requires that such content must be treated as
9581  // type "application/octet-stream".
9582  ct.type = ENC_CT_APPLICATION;
9584  ct.charset = ENC_CS_UNKNOWN;
9585  }
9586  break;
9587  }
9588  case ENC_CT_IMAGE:
9589  case ENC_CT_AUDIO:
9590  case ENC_CT_VIDEO:
9591  {
9592  break;
9593  }
9594  case ENC_CT_MULTIPART:
9595  {
9596  // The boundary delimiter is already stored in 'boundary'
9597  // Set flag and split the content later
9598  multi = true;
9599  // Set flag if subtype is "alternative"
9600  if(ENC_CTS_ALTERNATIVE == ct.subtype) { alt = true; }
9601  // Set flag if subtype is "digest"
9602  else if(ENC_CTS_DIGEST == ct.subtype) { digest = true; }
9603  break;
9604  }
9605  default:
9606  {
9607  // Handle anything unknown as type "application/octet-stream"
9608  ct.type = ENC_CT_APPLICATION;
9610  ct.charset = ENC_CS_UNKNOWN;
9611  break;
9612  }
9613  }
9614  }
9615  }
9616 
9617  // Create linked list of entities
9618  if(!multi)
9619  {
9620  multipart = false;
9621  partList = initElement(p, std::strlen(p), cte, &ct, h);
9622  partNum = 1;
9623  }
9624  else
9625  {
9626  multipart = true;
9627  // Recursively create elements from multipart entities
9628  partList = decodeElement(p, std::strlen(p),
9629  boundary, alt, digest, &partNum);
9630  }
9631 }
9632 
9633 
9634 // =============================================================================
9635 // MIME content object constructor
9636 
9637 MIMEContent::~MIMEContent(void)
9638 {
9639  MIMEContentListElement* cle = partList;
9640  MIMEContentListElement* nle;
9641 
9642  while(NULL != cle)
9643  {
9644  nle = cle->next;
9645  enc_free((void*) cle->filename);
9646  std::free((void*) cle->header);
9647  delete[] cle->content;
9648  delete cle;
9649  cle = nle;
9650  }
9651 }
9652 
9653 
9654 // =============================================================================
9655 // Subscribe tree OK button callback
9656 
9657 void SubscribeWindow::ok_cb_i(void)
9658 {
9659  Fl_Tree_Item* i = subscribeTree->first_selected_item();
9660  int rv = -1;
9661  std::size_t ii;
9662  char buf[1024];
9663  char* name;
9664 
9665  // Hide root node tree to remove it from pathname
9666  subscribeTree->showroot(0);
9667 
9668  // Update subscribed groups
9669  mainWindow->groupStateExport();
9670  while(NULL != i)
9671  {
9672  // Check whether an menu style path was stored in user data
9673  if('\0' != ((const char*) i->user_data())[0])
9674  {
9675  // Yes => Use it
9676  name = (char*) i->user_data();
9677  rv = 0;
9678  }
9679  else
9680  {
9681  // No => Get menu style path from widget
9682  rv = subscribeTree->item_pathname(buf, 1024, i);
9683  name = buf;
9684  }
9685  if(rv)
9686  {
9687  if(-2 == rv) { PRINT_ERROR("Group name too long and ignored"); }
9688  else { PRINT_ERROR("Group not found (bug)"); }
9689  }
9690  else
9691  {
9692  ii = 0;
9693  do
9694  { if('/' == name[ii]) { name[ii] = '.'; } }
9695  while(name[ii++]);
9696  // Because the number of selected groups is usually low, we can add
9697  // them one after the other without relevant performance impact
9698  rv = core_subscribe_group(name);
9699  if(rv)
9700  {
9701  PRINT_ERROR("Updating list of subscribed groups failed");
9702  break;
9703  }
9704  }
9705  i = subscribeTree->next_selected_item(i);
9706  rv = 0;
9707  }
9708 
9709  // Check result
9710  if(rv)
9711  {
9712  SC("Do not use non-ASCII for the translation of this item")
9713  fl_message_title(S("Error"));
9714  fl_alert("%s", S("Error while updating subscribed group database"));
9715  }
9716  else { UI_STATUS(S("Subscribed groups stored.")); }
9717 
9718  // This check should silcene code checking tools
9719  // ('mainWindow' is always present)
9720  if(NULL != mainWindow)
9721  {
9722  // Import new group list
9723  mainWindow->groupListImport();
9724  }
9725 
9726  // Schedule destruction of subscribe window
9727  Fl::delete_widget(this);
9728 }
9729 
9730 
9731 // =============================================================================
9732 // Subscribe window constructor
9733 
9734 SubscribeWindow::SubscribeWindow(const char* label, core_groupdesc* glist,
9735  core_grouplabel* labels) :
9736  UI_WINDOW_CLASS(730, 350, label), grouplist(glist), grouplabels(labels)
9737 {
9738  Fl_Group* gp;
9739  Fl_Box* bp;
9740  Fl_Button* p;
9741  int y, w, h, gy, gh, bw;
9742  int w1, w2, h1, h2;
9743 
9744  // Configure subscribe window
9745  copy_label(label);
9746  callback(cancel_cb, (void*) this);
9747  set_modal();
9748 
9749  // Add widgets --------------------------------------------------------------
9750  begin();
9751 
9752  // Create widget group
9753  subscribeGroup = new Fl_Group(0, 0, 730, 350);
9754  subscribeGroup->begin();
9755  {
9756  // Calculate required widths and heights
9757  gui_set_default_font();
9758  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
9759  fl_measure(S("OK"), w2 = 0, h2 = 0);
9760  if(w1 > w2) { w = w1; } else { w = w2; }
9761  if(h1 > h2) { h = h1; } else { h = h2; }
9762  w += 30;
9763  h += 10;
9764  gh = h + 10;
9765  gy = 350 - gh;
9766  // Add newsgroup tree
9767  subscribeTree = new Fl_Tree(0, 0, 730, 350 - gh);
9768  // Explicitly set foreground color to FLTK default
9769  // (default is not the same as for the other widgets)
9770  subscribeTree->item_labelfgcolor(FL_FOREGROUND_COLOR);
9771  // Special handling for the root item that was already present
9772  subscribeTree->root()->labelfgcolor(FL_FOREGROUND_COLOR);
9773  subscribeTree->root_label("Usenet");
9774  subscribeTree->margintop(5);
9775  subscribeTree->marginleft(0);
9776  subscribeTree->openchild_marginbottom(5);
9777  subscribeTree->selectmode(FL_TREE_SELECT_MULTI);
9778  subscribeTree->sortorder(FL_TREE_SORT_ASCENDING);
9779  subscribeTree->callback(tree_cb, (void*) this);
9780  // Add button bar at bottom
9781  gp = new Fl_Group(0, gy, 730, gh);
9782  gp->begin();
9783  {
9784  bw = w + 30;
9785  y = gy + 5;
9786  new Fl_Box(0, gy, bw, gh);
9787  p = new Fl_Button(15, y, w, h, S("OK"));
9788  p->callback(ok_cb, (void*) this);
9789  new Fl_Box(730 - bw, gy, bw, gh);
9790  p = new Fl_Button(730 - bw + 15, y, w, h, S("Cancel"));
9791  p->callback(cancel_cb, (void*) this);
9792  // Resizable space between buttons
9793  bp = new Fl_Box(bw, gy, 730 - (2 * bw), gh);
9794  }
9795  gp->end();
9796  gp->resizable(bp);
9797  }
9798  subscribeGroup->end();
9799  subscribeGroup->resizable(subscribeTree);
9800  resizable(subscribeGroup);
9801 
9802  end();
9803  // --------------------------------------------------------------------------
9804 
9805  // Set minimum window size
9806  size_range(2 * bw, 100, 0, 0);
9807 #if !CFG_DB_DISABLE
9808  Fl::visual(FL_DOUBLE | FL_INDEX);
9809 #endif // CFG_DB_DISABLE
9810  show();
9811 }
9812 
9813 
9814 // =============================================================================
9815 // Subscribe window destructor
9816 
9817 SubscribeWindow::~SubscribeWindow(void)
9818 {
9819  core_free((void*) grouplabels);
9820  core_free((void*) grouplist);
9821 }
9822 
9823 
9824 // =============================================================================
9825 // Protocol console update method
9826 
9827 void ProtocolConsole::update(void)
9828 {
9829  int rv;
9830  const char* logname;
9831  char buffer[128];
9832  unsigned int n;
9833 
9834  // Update protocol console
9835  if(!nolog)
9836  {
9837  if(!logfp)
9838  {
9839  logname = log_get_logpathname();
9840  if(logname)
9841  {
9842  logfp = fopen(logname, "r");
9843  log_free((void*) logname); logname = NULL;
9844  if(!logfp)
9845  {
9846  nolog = 1;
9847  protocolConsole->consoleDisplay->insert(
9848  S("Logfile is only available in debug mode.") );
9849  protocolConsole->consoleDisplay->insert("\n");
9850  protocolConsole->consoleDisplay->insert(
9851  S("Use command line option -debug for debug mode.") );
9852  }
9853  }
9854  }
9855  else
9856  {
9857  while(!feof(logfp))
9858  {
9859  for(n = 0; n < sizeof(buffer) - 1; ++n)
9860  {
9861  rv = fgetc(logfp);
9862  if(EOF == rv) { break; }
9863  else
9864  {
9865  // Replace CR characters with spaces
9866  if(0x0D == rv) { buffer[n] = 0x20; }
9867  else { buffer[n] = (char) (unsigned char) rv; }
9868  }
9869  }
9870  buffer[n] = 0;
9871  protocolConsole->consoleDisplay->insert(buffer);
9872  // Drive GUI
9873  Fl::check();
9874  // The protocol console was possibly destroyed by a callback
9875  // Verify that it's still present
9876  if(NULL == protocolConsole) { break; }
9877  }
9878  if(protocolConsole) { clearerr(logfp); }
9879  }
9880  }
9881 }
9882 
9883 
9884 // =============================================================================
9885 // Protocol console constructor
9886 
9887 ProtocolConsole::ProtocolConsole(const char* label) :
9888  UI_WINDOW_CLASS(730, 395, label), logfp(NULL), nolog(0)
9889 {
9890  copy_label(label);
9891  consoleText = new Fl_Text_Buffer();
9892  consoleDisplay = new Fl_Text_Display(0, 0, 730, 395);
9893  consoleDisplay->buffer(consoleText);
9894  resizable(consoleDisplay);
9895  callback(exit_cb, (void*) this);
9896 #if !CFG_DB_DISABLE
9897  Fl::visual(FL_DOUBLE | FL_INDEX);
9898 #endif // CFG_DB_DISABLE
9899  show();
9900 }
9901 
9902 
9903 // =============================================================================
9904 // Protocol console destructor
9905 
9906 ProtocolConsole::~ProtocolConsole(void)
9907 {
9908  if(logfp) { fclose(logfp); }
9909 
9910  // Release memory
9911  delete consoleDisplay;
9912  delete consoleText;
9913 
9914  protocolConsole = NULL;
9915 }
9916 
9917 
9918 // =============================================================================
9919 // Message-ID search window callback
9920 
9921 void MIDSearchWindow::ok_cb_i(void)
9922 {
9923  std::size_t limit = 248; // Octets (without angle brackets)
9924  const char* message_id = mid->value();
9925  char* buf = NULL;
9926  char* p;
9927  std::size_t len;
9928 
9929  // Copy Message-ID
9930  len = std::strlen(message_id);
9931  buf = new char[len + (std::size_t) 1];
9932  std::strcpy(buf, message_id);
9933 
9934  // Remove potential surrounding whitespace and angle brackets
9935  p = buf;
9936  while(0x09 == (int) p[0] || ' ' == p[0] || '<' == p[0])
9937  {
9938  p = &p[1];
9939  --len;
9940  }
9941  while(0x09 == (int) p[len - (std::size_t) 1]
9942  || ' ' == p[len - (std::size_t) 1] || '>' == p[len - (std::size_t) 1])
9943  {
9944  p[len - (std::size_t) 1] = 0;
9945  --len;
9946  }
9947 
9948  // Check length limit
9949  if(limit < len)
9950  {
9951  SC("Do not use non-ASCII for the translation of this item")
9952  fl_message_title(S("Error"));
9953  fl_alert("%s",
9954  S("Message-ID too long\nLimit is 250 characters, including angle brackets"));
9955  }
9956 
9957  // Destroy configuration window
9958  Fl::delete_widget(this);
9959 
9960  // Search article (and display it in separate window if found)
9961  mainWindow->viewArticle(UI_CB_START, (const char*) p);
9962  delete[] buf;
9963 }
9964 
9965 
9966 // =============================================================================
9967 // Message-ID search window constructor
9968 
9969 MIDSearchWindow::MIDSearchWindow(const char* label) :
9970  UI_WINDOW_CLASS(700, 150, label)
9971 {
9972  Fl_Group* gp1;
9973  Fl_Group* gp2;
9974  Fl_Box* bp;
9975  Fl_Button* p;
9976  int y, w, h, gy, gh, bw;
9977  int w1, w2, h1, h2;
9978 
9979  // Configure Message-ID search window
9980  copy_label(label);
9981  set_modal();
9982  callback(cancel_cb, (void*) this);
9983 
9984  // Create widget group
9985  cfgGroup = new Fl_Group(0, 0, 700, 150);
9986  cfgGroup->begin();
9987  {
9988  // Calculate required widths and heights
9989  gui_set_default_font();
9990  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
9991  fl_measure(S("OK"), w2 = 0, h2 = 0);
9992  if(w1 > w2) { w = w1; } else { w = w2; }
9993  if(h1 > h2) { h = h1; } else { h = h2; }
9994  w += 30;
9995  h += 10;
9996  gh = h + 10;
9997  gy = 150 - gh;
9998 
9999  gp1 = new Fl_Group(0, 0, 700, gy);
10000  gp1->begin();
10001  {
10002  // Add "From" Name field
10003  mid = new Fl_Input(15, 35, 670, 30, "Message-ID:");
10004  mid->align(FL_ALIGN_TOP_LEFT);
10005  // Resizable space below input fields
10006  bp = new Fl_Box(0, 100, 700, gy - 100);
10007  //bp->box(FL_DOWN_BOX);
10008  }
10009  gp1->end();
10010  gp1->resizable(bp);
10011  // Add button bar at bottom
10012  gp2 = new Fl_Group(0, gy, 700, gh);
10013  gp2->begin();
10014  {
10015  bw = w + 30;
10016  y = gy + 5;
10017  new Fl_Box(0, gy, bw, gh);
10018  p = new Fl_Return_Button(15, y, w, h, S("OK"));
10019  p->callback(ok_cb, (void*) this);
10020  new Fl_Box(500 - bw, gy, bw, gh);
10021  p = new Fl_Button(700 - bw + 15, y, w, h, S("Cancel"));
10022  p->callback(cancel_cb, (void*) this);
10023  p->shortcut(FL_Escape);
10024 
10025  // Resizable space between buttons
10026  bp = new Fl_Box(bw, gy, 700 - (2 * bw), gh);
10027  }
10028  gp2->end();
10029  gp2->resizable(bp);
10030  }
10031  cfgGroup->end();
10032  cfgGroup->resizable(gp1);
10033  resizable(cfgGroup);
10034  // Set minimum window size
10035  size_range(700, 100 + gh, 0, 0);
10036 #if !CFG_DB_DISABLE
10037  Fl::visual(FL_DOUBLE | FL_INDEX);
10038 #endif // CFG_DB_DISABLE
10039  show();
10040 }
10041 
10042 
10043 // =============================================================================
10044 // Message-ID search window destructor
10045 
10046 MIDSearchWindow::~MIDSearchWindow(void)
10047 {
10048 }
10049 
10050 
10051 // =============================================================================
10052 // Bug report window constructor
10053 
10054 BugreportWindow::BugreportWindow(const char* label, const char* content) :
10055  UI_WINDOW_CLASS(795, 395, label)
10056 {
10057  std::size_t len;
10058 
10059  copy_label(label);
10060  len = std::strlen(content);
10061  if(INT_MAX < len) { len = 0; }
10062  bugreportText = new Fl_Text_Buffer((int) ++len, 0);
10063  bugreportText->insert(0, content);
10064  bugreportDisplay = new Fl_Text_Display(0, 0, 795, 395);
10065  bugreportDisplay->textfont(FL_COURIER);
10066  bugreportDisplay->buffer(bugreportText);
10067  resizable(bugreportDisplay);
10068  callback(exit_cb, (void*) this);
10069 #if !CFG_DB_DISABLE
10070  Fl::visual(FL_DOUBLE | FL_INDEX);
10071 #endif // CFG_DB_DISABLE
10072  show();
10073 }
10074 
10075 
10076 // =============================================================================
10077 // Bug report window destructor
10078 
10079 BugreportWindow::~BugreportWindow(void)
10080 {
10081  // Release memory
10082  delete bugreportDisplay;
10083  delete bugreportText;
10084 }
10085 
10086 
10087 // =============================================================================
10088 // Message of the day window constructor
10089 
10090 MotdWindow::MotdWindow(const char* motd, const char* label) :
10091  UI_WINDOW_CLASS(795, 395, label)
10092 {
10093  copy_label(label);
10094  motdText = new Fl_Text_Buffer();
10095  motdText->text(motd);
10096  motdDisplay = new Fl_Text_Display(0, 0, 795, 395);
10097  motdDisplay->textfont(FL_COURIER);
10098  motdDisplay->buffer(motdText);
10099  resizable(motdDisplay);
10100  callback(exit_cb, (void*) this);
10101 #if !CFG_DB_DISABLE
10102  Fl::visual(FL_DOUBLE | FL_INDEX);
10103 #endif // CFG_DB_DISABLE
10104  show();
10105 }
10106 
10107 
10108 // =============================================================================
10109 // Message of the day window destructor
10110 
10111 MotdWindow::~MotdWindow(void)
10112 {
10113  // Release memory
10114  delete motdDisplay;
10115  delete motdText;
10116 }
10117 
10118 
10119 // =============================================================================
10120 // License window constructor
10121 
10122 LicenseWindow::LicenseWindow(const char* label) :
10123  UI_WINDOW_CLASS(795, 395, label)
10124 {
10125  int rv;
10126 
10127  copy_label(label);
10128  licenseText = new Fl_Text_Buffer();
10129  rv = licenseText->loadfile(CFG_LICENSE_PATH "/license.txt");
10130  if(rv) { licenseText->insert(0, S("Error: License file not found")); }
10131  licenseDisplay = new Fl_Text_Display(0, 0, 795, 395);
10132  licenseDisplay->textfont(FL_COURIER);
10133  licenseDisplay->buffer(licenseText);
10134  resizable(licenseDisplay);
10135  callback(exit_cb, (void*) this);
10136 #if !CFG_DB_DISABLE
10137  Fl::visual(FL_DOUBLE | FL_INDEX);
10138 #endif // CFG_DB_DISABLE
10139  show();
10140 }
10141 
10142 
10143 // =============================================================================
10144 // License window destructor
10145 
10146 LicenseWindow::~LicenseWindow(void)
10147 {
10148  // Release memory
10149  delete licenseDisplay;
10150  delete licenseText;
10151 }
10152 
10153 
10154 // =============================================================================
10155 // Article window format and print some header fields of article
10156 
10157 const char* ArticleWindow::printHeaderFields(struct core_article_header* h)
10158 {
10159  return(gui_print_header_fields(h));
10160 }
10161 
10162 
10163 // =============================================================================
10164 // Article window update
10165 
10166 void ArticleWindow::articleUpdate(Fl_Text_Buffer* article)
10167 {
10168  // See RFC 3986 for URI format
10169  const char* url[] = { "http://", "https://", "ftp://", "nntp://",
10170  "news:", "mailto:", NULL };
10171  const std::size_t url_len[] = { 7, 8, 6, 7, 5, 7 };
10172  const char bold = 'A'; // Bold text for header field names
10173  const char sig = 'B'; // Signature starting with "-- " separator
10174  const char cit = 'C'; // External citation ("|" at start of line)
10175  const char link = 'D'; // Hyperlink
10176  const char plain = 'E'; // Normal article text
10177  const char l1 = 'F'; // 1st citation level
10178  const char l2 = 'G'; // 2nd citation level
10179  const char l3 = 'H'; // 3rd citation level
10180  const char l4 = 'I'; // 4th citation level
10181  char* style;
10182  std::size_t len;
10183  std::size_t i;
10184  std::size_t ii = 0; // Index in current line
10185  std::size_t iii = 0;
10186  std::size_t iiii;
10187  std::size_t url_i;
10188  bool sol = true; // Start Of Line flag
10189  char hs = plain;
10190  bool ready = false; // Flag indicating positions beyond header separator
10191  int ss = 0;
10192  bool delim = false; // Hyperlink delimiter
10193  bool signature = false; // Flag indicating signature
10194  bool citation = false; // Flag indicating external citation
10195  bool hyperlink = false; // Flag indicating hyperlink
10196  std::size_t cl = 0;
10197  bool cl_lock = false;
10198  char c;
10199  int pos;
10200 
10201  // Replace current article
10202  if(NULL == article)
10203  {
10204  PRINT_ERROR("Article display request without content ignored (bug)");
10205  return;
10206  }
10207  gui_check_article(article);
10208  articleText = article;
10209  articleDisplay->buffer(articleText);
10210 
10211 #if !UI_AW_REFERENCES
10212  // Remove references line for ArticleWindow (not useful without hyperlinks)
10213  if (1 == articleText->findchar_forward(0, 0x1DU, &pos))
10214  {
10215  articleText->remove(articleText->line_start(pos),
10216  articleText->line_end(pos) + 1);
10217  }
10218 #endif // ! UI_AW_REFERENCES
10219 
10220  // Soft Hyphen (SHY) handling
10221  gui_process_shy(articleText);
10222 
10223  // Create article content style
10224  style = articleText->text();
10225  if(NULL == style) { len = 0; }
10226  else { len = std::strlen(style); }
10227  if(INT_MAX < len) { len = INT_MAX; }
10228  for(i = 0; i < len; ++i)
10229  {
10230  if('\n' == style[i])
10231  {
10232  sol = true;
10233  hyperlink = false;
10234  citation = false;
10235  continue;
10236  }
10237  if(hyperlink)
10238  {
10239  // Check for end of hyperlink
10240  // According to RFC 3986 whitespace, double quotes and angle brackets
10241  // are accepted as delimiters.
10242  // Whitespace is interpreted as SP or HT, Unicode whitespace is not
10243  // accepted as delimiter.
10244  c = style[i];
10245  if(' ' == c || 0x09 == (int) c || '>' == c || '"' == c)
10246  {
10247  hyperlink = false;
10248  }
10249  }
10250  // Highlight external citations (via '|' or '!')
10251  if(sol && ('|' == style[i] || '!' == style[i]))
10252  {
10253  if(!hyperlink) { citation = true; }
10254  }
10255  // Check for start of line
10256  if(sol) { sol = false; delim = true; ss = 0; cl = 0; ii = 0; }
10257  else { ++ii; }
10258  if(signature)
10259  {
10260  // Check for end of signature in potential multipart message
10261  if('|' == style[i])
10262  {
10263  if(79U == ii)
10264  {
10265  for(iiii = i - ii; iiii < i; ++iiii)
10266  {
10267  if('_' != articleText->byte_at((int) iiii)) { break; }
10268  }
10269  if(iiii == i)
10270  {
10271  for(iiii = 0; iiii <= ii; ++iiii)
10272  {
10273  style[i - iiii] = plain;
10274  }
10275  signature = false;
10276  }
10277  }
10278  }
10279  }
10280  if(!ready)
10281  {
10282  // Check for header separator <SOL>"____...____|"
10283  if(!ii)
10284  {
10285  if('_' != style[i]) { hs = bold; } else { hs = plain; }
10286  iii = 0;
10287  }
10288  if('|' == style[i] && 79U <= iii) { ready = true; }
10289  else if('_' == style[i]) { ++iii; }
10290  if(':' == style[i]) { hs = plain; }
10291  style[i] = hs;
10292  }
10293  else
10294  {
10295  // Check for signature separator <SOL>"-- "
10296  if('-' == style[i] && !ii) { ss = 1; }
10297  if('-' == style[i] && 1U == ii && 1 == ss) { ss = 2; }
10298  if(' ' == style[i] && 2U == ii && 2 == ss)
10299  {
10300  // Attention: This EOL check requires POSIX line format!
10301  if((char) 0x0A == style[i + 1U])
10302  {
10303  if(gui_last_sig_separator(&style[i + 1U]))
10304  {
10305  style[i] = sig; style[i - 1U] = sig; style[i - 2U] = sig;
10306  signature = true;
10307  }
10308  }
10309  }
10310  // Check for hyperlink
10311  url_i = 0;
10312  while(NULL != url[url_i])
10313  {
10314  if(!std::strncmp(&style[i], url[url_i], url_len[url_i]))
10315  {
10316  if(delim)
10317  {
10318  style[i] = link;
10319  // ArticleWindow does not support hyperlinks
10320  //hyperlink = true;
10321  }
10322  }
10323  ++url_i;
10324  }
10325  c = style[i];
10326  if(' ' == c || 0x09 == (int) c || '<' == c || '"' == c)
10327  {
10328  delim = true;
10329  }
10330  else { delim = false; }
10331  if(1)
10332  {
10333  // Highlight citation levels of regular content
10334  if(!ii)
10335  {
10336  if('>' == style[i]) { cl_lock = false; }
10337  else { cl_lock = true; }
10338  }
10339  if('>' == style[i] && !cl_lock) { ++cl; }
10340  if('>' != style[i] && ' ' != style[i] && (char) 9 != style[i])
10341  {
10342  cl_lock = true;
10343  }
10344  if(4U < cl) { cl = 1; } // Rotate colors if too many levels
10345  switch(cl)
10346  {
10347  case 1: { style[i] = l1; break; }
10348  case 2: { style[i] = l2; break; }
10349  case 3: { style[i] = l3; break; }
10350  case 4: { style[i] = l4; break; }
10351  default: { style[i] = plain; break; }
10352  }
10353  }
10354  // Override current style for signature, citations and hyperlinks
10355  // (hyperlinks have highest precedence)
10356  if(signature) { style[i] = sig; }
10357  if(citation && !signature) { style[i] = cit; }
10358  if(hyperlink) { style[i] = link; }
10359  }
10360  }
10361  articleStyle = new Fl_Text_Buffer((int) len, 0);
10362  if(NULL != style) { articleStyle->text(style); }
10363  std::free((void*) style);
10364 
10365  // Activate content style
10366  articleDisplay->highlight_data(articleStyle, styles, styles_len, 'A', NULL,
10367  NULL);
10368 
10369  // Replace potential GS marker with SP
10370  if (1 == articleText->findchar_forward(0, 0x1DU, &pos))
10371  {
10372  articleText->replace(pos, pos + 1, " ");
10373  }
10374 }
10375 
10376 
10377 // =============================================================================
10378 // Article window constructor
10379 
10380 ArticleWindow::ArticleWindow(const char* article, const char* label) :
10381  UI_WINDOW_CLASS(795, 395, label)
10382 {
10383  Fl_Color signature = conf_integer_check(CONF_COLOR_SIGNATURE, 0x00, 0xFF);
10384  Fl_Color external = conf_integer_check(CONF_COLOR_EXTERNAL, 0x00, 0xFF);
10385  Fl_Color level1 = conf_integer_check(CONF_COLOR_LEVEL1, 0x00, 0xFF);
10386  Fl_Color level2 = conf_integer_check(CONF_COLOR_LEVEL2, 0x00, 0xFF);
10387  Fl_Color level3 = conf_integer_check(CONF_COLOR_LEVEL3, 0x00, 0xFF);
10388  Fl_Color level4 = conf_integer_check(CONF_COLOR_LEVEL4, 0x00, 0xFF);
10389  const Fl_Color styles_colors[UI_STYLES_LEN] =
10390  {
10391  FL_FOREGROUND_COLOR, // Plain bold
10392  signature, // Signature
10393  external, // External citation
10394  FL_BLUE, // Hyperlink (never change this color!)
10395  // -----------------------------------------------------------------------
10396  // Keep the following group continuous
10397  FL_FOREGROUND_COLOR, // Content
10398  level1, // Thread citation level 1 ("> ")
10399  level2, // Thread citation level 2 ("> > ")
10400  level3, // Thread citation level 3 ("> > > ")
10401  level4 // Thread citation level 4 ("> > > > ")
10402  };
10403  Fl_Group* gp;
10404  Fl_Box* bp;
10405  Fl_Button* buttonp;
10406  int y, w, h, gy, gh, bw;
10407  int rv = -1;
10408  char* raw;
10409  char* eoh;
10410  const char* q = NULL;
10411  Fl_Text_Buffer* tb;
10412  const char* hdr = NULL;
10413  int ii;
10414 
10415  copy_label(label);
10416 
10417  // Init alternate article hierarchy pointer
10418  alt_hier = NULL;
10419 
10420  // Init text buffer pointers
10421  articleText = NULL;
10422  articleStyle = NULL;
10423  styles = NULL;
10424 
10425  // Init MIME object pointer
10426  mimeData = NULL;
10427 
10428  // Create new article hierarchy
10429  rv = core_hierarchy_manager(&alt_hier, CORE_HIERARCHY_INIT, 0);
10430  if(!rv)
10431  {
10432  // Add article to alternative hierarchy
10433  rv = core_hierarchy_manager(&alt_hier, CORE_HIERARCHY_ADD, 1, article);
10434  }
10435  if(!rv)
10436  {
10437  raw = new char[std::strlen(article) + (std::size_t) 1];
10438  std::strcpy(raw, article);
10439  // Search for end of header
10440  // Attention: Overloaded and both prototypes different than in C!
10441  eoh = std::strstr(raw, "\r\n\r\n");
10442  if(NULL == eoh)
10443  {
10444  // Body not found
10445  PRINT_ERROR("Processing of article failed");
10446  SC("Do not use non-ASCII for the translation of this item")
10447  fl_message_title(S("Error"));
10448  fl_alert("%s", S("Processing of article failed"));
10449  delete[] raw;
10450  }
10451  else
10452  {
10453  // Set pointer to body data
10454  q = &eoh[4];
10455  // Create text buffer
10456  tb = new Fl_Text_Buffer(0, 0);
10457  // Print formatted header data to text buffer
10458  hdr = printHeaderFields(alt_hier->child[0]->header);
10459  if(NULL != hdr)
10460  {
10461  tb->text(hdr);
10462  delete[] hdr;
10463  }
10464  // Print header/body delimiter
10465  tb->append(ENC_DELIMITER);
10466 
10467  // Create MIME object from content
10468  mimeData = new MIMEContent(alt_hier->child[0]->header, q);
10469  delete[] raw;
10470  gui_decode_mime_entities(tb, mimeData,
10471  alt_hier->child[0]->header->msgid);
10472 
10473  // Configure article window
10474  callback(cancel_cb, (void*) this);
10475  set_modal();
10476 
10477  // Add widgets --------------------------------------------------------
10478  begin();
10479 
10480  // Create widget group
10481  articleGroup = new Fl_Group(0, 0, 795, 395);
10482  articleGroup->begin();
10483  {
10484  // Calculate required widths and heights
10485  gui_set_default_font();
10486  fl_measure(S("Cancel"), w = 0, h = 0);
10487  w += 30;
10488  h += 10;
10489  gh = h + 10;
10490  gy = 395 - gh;
10491  // Add text field
10492  articleDisplay = new My_Text_Display(0, 0, 795, 395 - gh);
10493  articleDisplay->textfont(FL_COURIER);
10494  // Allocate and init styles
10495  styles_len = UI_STYLES_LEN;
10496  styles = new Fl_Text_Display::Style_Table_Entry[styles_len];
10497  styles[0].color = FL_FOREGROUND_COLOR;
10498  styles[0].font = FL_COURIER_BOLD;
10499  styles[0].size = articleDisplay->textsize();
10500  styles[0].attr = 0;
10501  for(ii = 1; ii < styles_len; ++ii)
10502  {
10503  styles[ii].color = styles_colors[ii];
10504  styles[ii].font = articleDisplay->textfont();
10505  styles[ii].size = articleDisplay->textsize();
10506  styles[ii].attr = 0;
10507  }
10508  // Set text buffer
10509  articleUpdate(tb);
10510  resizable(articleDisplay);
10511  // Add button bar at bottom
10512  gp = new Fl_Group(0, gy, 795, gh);
10513  gp->begin();
10514  {
10515  bw = w + 30;
10516  y = gy + 5;
10517  new Fl_Box(0, gy, bw, gh);
10518  new Fl_Box(795 - bw, gy, bw, gh);
10519  buttonp = new Fl_Button(795 - bw + 15, y, w, h, S("Cancel"));
10520  buttonp->callback(cancel_cb, (void*) this);
10521  // Resizable space between buttons
10522  bp = new Fl_Box(bw, gy, 795 - (2 * bw), gh);
10523  }
10524  gp->end();
10525  gp->resizable(bp);
10526  }
10527  articleGroup->end();
10528  articleGroup->resizable(articleDisplay);
10529  resizable(articleGroup);
10530 
10531  end();
10532  // --------------------------------------------------------------------
10533 
10534  // Set minimum window size
10535  size_range(2 * bw, 100, 0, 0);
10536 #if !CFG_DB_DISABLE
10537  Fl::visual(FL_DOUBLE | FL_INDEX);
10538 #endif // CFG_DB_DISABLE
10539  show();
10540  }
10541  }
10542 }
10543 
10544 
10545 // =============================================================================
10546 // Article source window destructor
10547 
10548 ArticleWindow::~ArticleWindow(void)
10549 {
10550  if(NULL != mimeData) { delete mimeData; }
10551  // Detach text buffers and destroy them
10552  articleDisplay->highlight_data(dummyTb, NULL, 0, 'A', NULL, NULL);
10553  if(articleStyle) delete articleStyle;
10554  if(styles) { delete[] styles; }
10555  articleDisplay->buffer(dummyTb);
10556  if(articleText) delete articleText;
10557  // Destroy alternative article hierarchy
10559 }
10560 
10561 
10562 // =============================================================================
10563 // Article source window save file
10564 
10565 void ArticleSrcWindow::save_cb_i(void)
10566 {
10567  int rv;
10568 
10569  SC("Do not use characters for the translation that cannot be")
10570  SC("converted to the ISO 8859-1 character set for this item.")
10571  SC("Leave the original string in place if in doubt.")
10572  const char* title = S("Save article source code");
10573  const char* suggested_pathname = core_suggest_pathname();
10574 
10575  fl_file_chooser_ok_label(S("Save"));
10576  if(NULL != suggested_pathname)
10577  {
10578  pathname = fl_file_chooser(title, "*", suggested_pathname, 0);
10579  if(NULL != pathname)
10580  {
10581  // Ask user for overwrite permission if file exists
10582  rv = gui_check_pathname(pathname);
10583  if (0 == rv)
10584  {
10585  // Save to file
10586  rv = core_save_to_file(pathname, srcArticle);
10587  if(rv)
10588  {
10589  SC("Do not use non-ASCII for the translation of this item")
10590  fl_message_title(S("Error"));
10591  fl_alert("%s", S("Operation failed"));
10592  }
10593  }
10594  }
10595  core_free((void*) suggested_pathname);
10596  }
10597 }
10598 
10599 
10600 // =============================================================================
10601 // Article source window constructor
10602 
10603 ArticleSrcWindow::ArticleSrcWindow(const char* article, const char* label) :
10604  UI_WINDOW_CLASS(795, 395, label)
10605 {
10606  Fl_Group* gp;
10607  Fl_Box* bp;
10608  Fl_Button* p;
10609  int y, w, h, gy, gh, bw;
10610  int w1, w2, h1, h2;
10611  std::size_t len;
10612  const char* ap = NULL;
10613  int i = 0;
10614  int buflen;
10615  unsigned int octet;
10616  char num[3];
10617  const char* style;
10618  int toggle = 0;
10619 
10620  copy_label(label);
10621 
10622  // Copy raw article content to local memory for "Save to file" operation
10623  len = std::strlen(article);
10624  srcArticle = new char[++len];
10625  std::strcpy(srcArticle, article);
10626 
10627  // Convert article content line breaks from canonical (RFC 822) form
10628  // to POSIX form
10629  srcText = new Fl_Text_Buffer();
10630  srcStyle = new Fl_Text_Buffer();
10631  ap = core_convert_canonical_to_posix(article, 0, 0);
10632  if(NULL == ap) { srcText->insert(0, "Conversion to POSIX form failed"); }
10633  else
10634  {
10635  srcText->insert(0, ap);
10636  // Replace control/non-ASCII octets with colored hexadecimal numbers
10637  buflen = srcText->length();
10638  while(buflen > i)
10639  {
10640  srcStyle->insert(i, "A");
10641  octet = (unsigned int) (unsigned char) srcText->byte_at(i);
10642  num[0] = (char) (unsigned char) octet;
10643  num[1] = 0;
10644  if( (0x0AU != octet && enc_ascii_check_printable(num))
10645  || 0x09U == octet )
10646  {
10647  enc_convert_octet_to_hex(num, octet);
10648  srcText->replace(i, i + 1, num);
10649  if(!toggle) { style = "BB"; } else { style = "CC"; }
10650  srcStyle->replace(i, i + 1, style);
10651  buflen = srcText->length();
10652  ++i;
10653  toggle = !toggle;
10654  }
10655  else { toggle = 0; }
10656  ++i;
10657  }
10658  core_free((void*) ap);
10659  gui_check_article(srcText);
10660  }
10661 
10662  // Configure article source code window
10663  callback(cancel_cb, (void*) this);
10664  set_modal();
10665 
10666  // Add widgets --------------------------------------------------------------
10667  begin();
10668 
10669  // Create widget group
10670  srcGroup = new Fl_Group(0, 0, 795, 395);
10671  srcGroup->begin();
10672  {
10673  // Calculate required widths and heights
10674  gui_set_default_font();
10675  fl_measure(S("Cancel"), w1 = 0, h1 = 0);
10676  fl_measure(S("Save"), w2 = 0, h2 = 0);
10677  if(w1 > w2) { w = w1; } else { w = w2; }
10678  if(h1 > h2) { h = h1; } else { h = h2; }
10679  w += 30;
10680  h += 10;
10681  gh = h + 10;
10682  gy = 395 - gh;
10683  // Add text field
10684  srcDisplay = new Fl_Text_Display(0, 0, 795, 395 - gh);
10685  srcDisplay->textfont(FL_COURIER);
10686  srcDisplay->buffer(srcText);
10687  resizable(srcDisplay);
10688  // Create content and style buffers
10689  styles = new Fl_Text_Display::Style_Table_Entry[3];
10690  styles[0].color = FL_FOREGROUND_COLOR;
10691  styles[0].font = FL_COURIER;
10692  styles[0].size = srcDisplay->textsize();
10693  styles[0].attr = 0;
10694  styles[1].color = FL_COLOR_CUBE + (Fl_Color) 35;
10695  styles[1].font = FL_COURIER;
10696  styles[1].size = srcDisplay->textsize();
10697  styles[1].attr = 0;
10698  styles[2].color = FL_RED;
10699  styles[2].font = FL_COURIER;
10700  styles[2].size = srcDisplay->textsize();
10701  styles[2].attr = 0;
10702  srcDisplay->highlight_data(srcStyle, styles, 3, 'A', NULL, NULL);
10703  // Add button bar at bottom
10704  gp = new Fl_Group(0, gy, 795, gh);
10705  gp->begin();
10706  {
10707  bw = w + 30;
10708  y = gy + 5;
10709  new Fl_Box(0, gy, bw, gh);
10710  p = new Fl_Button(15, y, w, h, S("Save"));
10711  p->callback(save_cb, (void*) this);
10712  new Fl_Box(795 - bw, gy, bw, gh);
10713  p = new Fl_Button(795 - bw + 15, y, w, h, S("Cancel"));
10714  p->callback(cancel_cb, (void*) this);
10715  // Resizable space between buttons
10716  bp = new Fl_Box(bw, gy, 795 - (2 * bw), gh);
10717  }
10718  gp->end();
10719  gp->resizable(bp);
10720  }
10721  srcGroup->end();
10722  srcGroup->resizable(srcDisplay);
10723  resizable(srcGroup);
10724 
10725  end();
10726  // --------------------------------------------------------------------------
10727 
10728  // Set minimum window size
10729  size_range(2 * bw, 100, 0, 0);
10730 #if !CFG_DB_DISABLE
10731  Fl::visual(FL_DOUBLE | FL_INDEX);
10732 #endif // CFG_DB_DISABLE
10733  show();
10734 }
10735 
10736 
10737 // =============================================================================
10738 // Article source window destructor
10739 
10740 ArticleSrcWindow::~ArticleSrcWindow(void)
10741 {
10742  // Detach text buffers and destroy them
10743  srcDisplay->highlight_data(dummyTb, NULL, 0, 'A', NULL, NULL);
10744  delete srcStyle;
10745  delete[] styles;
10746  srcDisplay->buffer(dummyTb);
10747  delete srcText;
10748  delete[] srcArticle;
10749 }
10750 
10751 
10752 // =============================================================================
10753 // Compose window header parser
10754 //
10755 // \param[in] name Header field name
10756 // \param[out] start Start of header field (BOL)
10757 // \param[out] end End of header field (LF character at EOL)
10758 //
10759 // This method searches the header field \e name in the article header.
10760 
10761 int ComposeWindow::searchHeaderField(const char* name, int* start, int* end)
10762 {
10763  int res = -1;
10764  int rv;
10765  unsigned int c = (unsigned int) '\n';
10766  int sp = 0;
10767 
10768  while(1)
10769  {
10770  rv = compHeader->search_forward(sp, name, start, 1);
10771  if(1 == rv)
10772  {
10773  // Name match found => Verify that it starts at BOL
10774  if(0 < *start) { c = compHeader->char_at(*start - 1); }
10775  if((unsigned int) '\n' != c)
10776  {
10777  // No => Continue searching
10778  sp = *start + 1;
10779  continue;
10780  }
10781  else
10782  {
10783  // Yes => Search for EOL
10784  rv = compHeader->findchar_forward(*start, (unsigned int) '\n', end);
10785  if(1 == rv) { res = 0; }
10786  // Check for folding and include potential folded lines
10787  while(!res)
10788  {
10789  if((unsigned int) ' ' == compHeader->char_at(*end + 1)
10790  && (unsigned int) '\n' != compHeader->char_at(*end + 2))
10791  {
10792  rv = compHeader->findchar_forward(*end + 1,
10793  (unsigned int) '\n', end);
10794  if(1 != rv) { res = -1; }
10795  }
10796  else { break; }
10797  }
10798  }
10799  }
10800  break;
10801  }
10802 
10803  return(res);
10804 }
10805 
10806 // =============================================================================
10807 // Compose window header parser
10808 //
10809 // \param[in] name Header field name
10810 //
10811 // This method extracts the header field "name" from the article header.
10812 //
10813 // On success the caller is responsible for releasing the memory allocated for
10814 // the result using \c std::free() .
10815 //
10816 // \returns
10817 // - Pointer to result buffer
10818 // - NULL on error
10819 
10820 const char* ComposeWindow::extractHeaderField(const char* name)
10821 {
10822  char* res = NULL;
10823  int rv;
10824  int start;
10825  int end;
10826  int pos;
10827 
10828  rv = searchHeaderField(name, &start, &end);
10829  if(!rv)
10830  {
10831  // Search for start of header field body
10832  rv = compHeader->search_forward(start, ": ", &pos, 1);
10833  if(1 == rv)
10834  {
10835  pos += 2;
10836  res = compHeader->text_range(pos, end);
10837  }
10838  }
10839 
10840  return(res);
10841 }
10842 
10843 
10844 // =============================================================================
10845 // Compose window header parser
10846 //
10847 // \param[in] name Header field name
10848 // \param[in] new_body New body for header field \e name
10849 //
10850 // This method replaces the body of the header field \e name in the article
10851 // header.
10852 
10853 int ComposeWindow::replaceHeaderField(const char* name, const char* new_body)
10854 {
10855  int res = -1;
10856  int rv;
10857  int start;
10858  int end;
10859  int pos;
10860 
10861  rv = searchHeaderField(name, &start, &end);
10862  if(!rv)
10863  {
10864  // Search for start of header field body
10865  rv = compHeader->search_forward(start, ": ", &pos, 1);
10866  if(1 == rv)
10867  {
10868  pos += 2;
10869  if(end >= pos)
10870  {
10871  // Replace header field body
10872  compHeader->replace(pos, end, new_body);
10873  res = 0;
10874  }
10875  }
10876  }
10877 
10878  return(res);
10879 }
10880 
10881 
10882 // =============================================================================
10883 // Compose window header parser
10884 //
10885 // \param[in] name Header field name
10886 //
10887 // This method deletes the header field \e name in the article header.
10888 
10889 void ComposeWindow::deleteHeaderField(const char* name)
10890 {
10891  int rv;
10892  int start;
10893  int end;
10894 
10895  rv = searchHeaderField(name, &start, &end);
10896  if(!rv) { compHeader->remove(start, end + 1); }
10897 
10898  return;
10899 }
10900 
10901 
10902 // =============================================================================
10903 // Compose window body checker
10904 //
10905 // \param[in] body Article body to check
10906 //
10907 // This method verify that \e body contains own content.
10908 
10909 int ComposeWindow::checkArticleBody(const char* body)
10910 {
10911  int res = -1;
10912  const char* p;
10913  const char* sig_delim;
10914  const char* sig = NULL;
10915  std::size_t len;
10916  std::size_t i = 0;
10917  bool sol = false; // Start Of Line flag (ignore first line)
10918  bool check = false;
10919  std::size_t lines = 0;
10920 
10921  if(NULL != body)
10922  {
10923  // Calculate length without signature
10924  len = std::strlen(body);
10925  p = body;
10926  while(1)
10927  {
10928  // Attention: Overloaded and both prototypes different than in C!
10929  sig_delim = std::strstr(p, "\n-- \n");
10930  if(NULL == sig_delim) { break; }
10931  else { sig = p = &sig_delim[1]; }
10932  }
10933  if(NULL != sig) { len = (std::size_t) (sig - body); }
10934  // Search for own (not cited, non-whitespace) content
10935  for(i = 0; i < len; ++i)
10936  {
10937  if(0x0A == (int) body[i]) { sol = true; check = false; continue; }
10938  if(sol) { ++lines; }
10939  if(sol && '>' != body[i]) { check = true; }
10940  sol = false;
10941  if(check)
10942  {
10943  // Check content of non-citation line
10944  // SP and HT is not treated as relevant content
10945  if(' ' != body[i] && 0x09 != (int) body[i]) { res = 0; break; }
10946  }
10947  }
10948  // Special check for messages that contain only one line
10949  if(res && (std::size_t) 1 >= lines && '>' != body[0]) { res = 0; }
10950  }
10951 
10952  return(res);
10953 }
10954 
10955 
10956 // =============================================================================
10957 // Compose window Change subject button callback
10958 
10959 void ComposeWindow::change_cb_i(Fl_Widget* w)
10960 {
10961  const char* subject;
10962  const char* subject_new;
10963  char* p;
10964  const char* q;
10965  std::size_t len = 8; // Length of " (was: )"
10966 
10967  // Get old subject
10968  subject = subjectField->value();
10969  // Get new subject
10970  subject_new = fl_input("%s", "", S("New subject:"));
10971  if(NULL != subject_new)
10972  {
10973  if(std::strlen(subject_new))
10974  {
10975  // Check whether subject is changed again
10976  // Attention: Overloaded and both prototypes different than in C!
10977  q = std::strstr(subject, " (was: ");
10978  if(NULL != q)
10979  {
10980  // Yes => Preserve " (was: ..." part in parenthesis
10981  len += std::strlen(subject_new) + std::strlen(q);
10982  p = new char[++len];
10983  std::strcpy(p, subject_new);
10984  std::strcat(p, q);
10985  subjectField->value(p);
10986  delete[] p;
10987  }
10988  else
10989  {
10990  // No => Strip potential "Re: " prefix from old subject
10991  // Attention: Overloaded and both prototypes different than in C!
10992  q = std::strstr(subject, "Re: ");
10993  if(NULL != q) { subject = &subject[4]; }
10994  len += std::strlen(subject_new) + std::strlen(subject);
10995  p = new char[++len];
10996  std::strcpy(p, subject_new);
10997  std::strcat(p, " (was: ");
10998  std::strcat(p, subject);
10999  std::strcat(p, ")");
11000  subjectField->value(p);
11001  delete[] p;
11002  }
11003  }
11004  }
11005 }
11006 
11007 
11008 // =============================================================================
11009 // Compose window style update callback
11010 
11011 void ComposeWindow::style_update_cb_i(int pos, int nInserted, int nDeleted,
11012  int nRestyled, const char* deletedText,
11013  Fl_Text_Buffer* style,
11014  Fl_Text_Editor* editor)
11015 {
11016  char sbuf[UI_STATIC_STYLE_BUFSIZE + (std::size_t) 1];
11017  char* buf;
11018  int start, end;
11019  int lines;
11020  int i;
11021  std::size_t len;
11022  int pil;
11023  int next;
11024  std::size_t p72, p78;
11025 
11026  // Check for parameter integrity
11027  if(0 > nInserted || 0 > nDeleted)
11028  {
11029  PRINT_ERROR("Invalid parameter in compose window style update CB");
11030  return;
11031  }
11032 
11033  // If this is just a selection change => Ignore
11034  if(!nInserted && !nDeleted) { return; }
11035 
11036  // Check whether text was inserted or deleted
11037  if(nInserted)
11038  {
11039  // Use buffer on stack if new data is small enough
11040  if(UI_STATIC_STYLE_BUFSIZE < (std::size_t) nInserted)
11041  {
11042  buf = new char[(std::size_t) (nInserted + 1)];
11043  }
11044  else { buf = sbuf; }
11045  std::memset((void*) buf, 'A', (std::size_t) nInserted);
11046  buf[nInserted] = 0;
11047  style->replace(pos, pos + nDeleted, buf);
11048  if(UI_STATIC_STYLE_BUFSIZE < (std::size_t) nInserted) { delete[] buf; }
11049  }
11050  else if(nDeleted)
11051  {
11052  style->remove(pos, pos + nDeleted);
11053  }
11054 
11055  // Restyle new data and up to next linefeed
11056  start = editor->buffer()->line_start(pos);
11057  end = editor->buffer()->line_end(pos + nInserted);
11058  lines = 1 + editor->buffer()->count_lines(pos, end);
11059  for(i = 0; i < lines; ++i)
11060  {
11061  buf = editor->buffer()->line_text(start);
11062  if(NULL == buf) { continue; }
11063  len = std::strlen(buf);
11064  if(len)
11065  {
11066  std::memset((void*) buf, 'A', len);
11067 
11068  next = start;
11069  pil = 0;
11070  p72 = 0;
11071  p78 = len;
11072  while((std::size_t) next - (std::size_t) start < len)
11073  {
11074  if(72 == pil) { p72 = (std::size_t) next - (std::size_t) start; }
11075  if(78 == pil++) { p78 = (std::size_t) next - (std::size_t) start; }
11076  next = editor->buffer()->next_char(next);
11077  }
11078  if(p72)
11079  {
11080  std::memset((void*) &buf[p72], 'B', p78 - p72);
11081  }
11082  if(len - p78)
11083  {
11084  std::memset((void*) &buf[p78], 'C', len - p78);
11085  }
11086 
11087  style->replace(start, start + (int) len, buf);
11088  }
11089  start = editor->buffer()->skip_lines(start, 1);
11090  std::free((void*) buf);
11091  }
11092 
11093  // Update editor content
11094  editor->redisplay_range(pos, end);
11095 }
11096 
11097 
11098 // =============================================================================
11099 // Compose window Cancel button callback
11100 
11101 void ComposeWindow::cancel_cb_i(Fl_Widget* w)
11102 {
11103  // Ignore escape key
11104  if(Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
11105  {
11106  return;
11107  }
11108  // Destroy compose window
11109  mainWindow->composeWindow = NULL;
11110  Fl::delete_widget(w);
11111  // Update main window state
11112  mainWindow->composeComplete();
11113 }
11114 
11115 
11116 // =============================================================================
11117 // Compose window Send button callback
11118 //
11119 // Note: Article is still in local (not canonical) form here
11120 
11121 void ComposeWindow::send_cb_i(Fl_Widget* w)
11122 {
11123  const char* fqdn = config[CONF_FQDN].val.s;
11124  char* p;
11125  std::size_t len;
11126  std::size_t i;
11127  std::size_t lenh;
11128  std::size_t lenb;
11129  int rv = 0;
11130  const char* q;
11131  const char* newsgroups;
11132  const char* subject;
11133  const char* organization;
11134  const char* msgid;
11135  const char* cancel_lock1;
11136  const char* cancel_lock;
11137  const char* date;
11138  const char* injection_date;
11139  const char* header;
11140  const char* body;
11141  const char* expires;
11142  bool free_subject = false;
11143  int start;
11144  int end;
11145  bool fup2_present = false;
11146 
11147  // Replace header field "Newsgroups" with potentially modified data
11148  newsgroups = newsgroupsField->value();
11149  len = std::strlen(newsgroups);
11150  if(!len)
11151  {
11152  SC("Do not use non-ASCII for the translation of this item")
11153  fl_message_title(S("Error"));
11154  fl_alert("%s", S("Newsgroups list is empty"));
11155  rv = -1;
11156  }
11157  else
11158  {
11159  // Check content according to RFC 5536
11160  q = newsgroups;
11161  for(i = 0; i < len; ++i)
11162  {
11163  if(',' == q[i] || '.' == q[i]
11164  || '+' == q[i] || '-' == q[i] || '_' == q[i])
11165  {
11166  continue;
11167  }
11168  if(0x30 <= q[i] && 0x39 >= q[i]) { continue; }
11169  if(0x41 <= q[i] && 0x5A >= q[i]) { continue; }
11170  if(0x61 <= q[i] && 0x7A >= q[i]) { continue; }
11171  rv = -1;
11172  break;
11173  }
11174  if(',' == q[0] || ',' == q[len - (std::size_t) 1]) { rv = -1; }
11175  if('.' == q[0] || '.' == q[len - (std::size_t) 1]) { rv = -1; }
11176  if(rv)
11177  {
11178  SC("Do not use non-ASCII for the translation of this item")
11179  fl_message_title(S("Error"));
11180  fl_alert("%s", S("Invalid content in Newsgroups header field"));
11181  }
11182  else
11183  {
11184  rv = replaceHeaderField("Newsgroups", newsgroups);
11185  if(rv)
11186  {
11187  SC("Do not use non-ASCII for the translation of this item")
11188  fl_message_title(S("Error"));
11189  fl_alert("%s", S("Replacement of Newsgroups in header failed"));
11190  rv = -1;
11191  }
11192  }
11193  }
11194 
11195  // Replace header field "Subject" with potentially modified data
11196  if(!rv)
11197  {
11198  subject = subjectField->value();
11199  if(!std::strlen(subject))
11200  {
11201  SC("Do not use non-ASCII for the translation of this item")
11202  fl_message_title(S("Error"));
11203  fl_alert("%s", S("Subject is empty"));
11204  rv = -1;
11205  }
11206  else
11207  {
11208  rv = enc_mime_word_encode(&q, subject, std::strlen("Subject: "));
11209  if(0 <= rv)
11210  {
11211  if(!rv) { free_subject = true; }
11212  subject = q;
11213  rv = replaceHeaderField("Subject", subject);
11214  if(rv)
11215  {
11216  SC("Do not use non-ASCII for the translation of this item")
11217  fl_message_title(S("Error"));
11218  fl_alert("%s", S("Replacement of Subject in header failed"));
11219  rv = -1;
11220  }
11221  if(free_subject) { enc_free((void*) subject); }
11222  }
11223  }
11224  }
11225 
11226  // Encode optional header field "Organization" that may be potentially present
11227  if(!rv)
11228  {
11229  organization = extractHeaderField("Organization");
11230  if(NULL != organization)
11231  {
11232  // Verify UTF-8 encoding
11233  rv = enc_uc_check_utf8(organization);
11234  if(!rv)
11235  {
11236  rv = enc_mime_word_encode(&q, organization,
11237  std::strlen("Organization: "));
11238  if(0 < rv) { rv = 0; }
11239  else if(!rv)
11240  {
11241  rv = replaceHeaderField("Organization", q);
11242  enc_free((void*) q);
11243  }
11244  }
11245  std::free((void*) organization);
11246  if(rv)
11247  {
11248  SC("Do not use non-ASCII for the translation of this item")
11249  fl_message_title(S("Error"));
11250  fl_alert("%s", S("Encoding of Organization header field failed"));
11251  rv = -1;
11252  }
11253  }
11254  }
11255 
11256  // Insert, replace or remove optional header fields before MIME declaration
11257  if(!rv)
11258  {
11259  rv = searchHeaderField("MIME-Version", &start, &end);
11260  if(rv)
11261  {
11262  SC("Do not use non-ASCII for the translation of this item")
11263  fl_message_title(S("Error"));
11264  fl_alert("%s", S("Bug: Mandatory MIME declaration not found"));
11265  rv = -1;
11266  }
11267  else
11268  {
11269  // Archive
11270  if(!archiveButton->value())
11271  {
11272  if(replaceHeaderField("Archive", "no"))
11273  {
11274  compHeader->insert(start, "Archive: no\n");
11275  }
11276  }
11277  else { deleteHeaderField("Archive"); }
11278  // Distribution
11279  q = distriField->value();
11280  if(NULL != q)
11281  {
11282  len = std::strlen(q);
11283  if(len)
11284  {
11285  // Check content (incomplete)
11286  for(i = 0; i < len; ++i)
11287  {
11288  if(',' == q[i] || '+' == q[i] || '-' == q[i] || '_' == q[i])
11289  {
11290  continue;
11291  }
11292  if(0x30 <= q[i] && 0x39 >= q[i]) { continue; }
11293  if(0x41 <= q[i] && 0x5A >= q[i]) { continue; }
11294  if(0x61 <= q[i] && 0x7A >= q[i]) { continue; }
11295  rv = -1;
11296  break;
11297  }
11298  if(',' == q[0] || ',' == q[len - (std::size_t) 1]) { rv = -1; }
11299  // RFC 5536 forbids use of "all" and recommends to not use
11300  // "world" (because this is the default behaviour, the header
11301  // field is not created in this case)
11302  p = (char*) std::malloc(len + (std::size_t) 1);
11303  if(NULL == p) { rv = -1; }
11304  else
11305  {
11306  // Convert content to lower case
11307  for(i = 0; i < len; ++i)
11308  {
11309  // Note: Behaviour is undefined if argument is not
11310  // representable as unsigned char or is not equal to EOF.
11311 #if defined(__cplusplus) && __cplusplus >= 199711L
11312  p[i] = (char) std::tolower((int) (unsigned char) q[i]);
11313 #else // defined(__cplusplus) && __cplusplus >= 199711L
11314  // Pre-C++98 compilers may have problems with the namespace
11315  p[i] = (char) tolower((int) (unsigned char) q[i]);
11316 #endif // defined(__cplusplus) && __cplusplus >= 199711L
11317  }
11318  p[len] = 0;
11319  // Check for "all"
11320  // Attention:
11321  // Overloaded and both prototypes different than in C!
11322  if(NULL != std::strstr(p, "all")) { rv = -1; }
11323  }
11324  // Check for error
11325  if(rv)
11326  {
11327  SC("Do not use non-ASCII for the translation of this item")
11328  fl_message_title(S("Error"));
11329  fl_alert("%s",
11330  S("Invalid content in Distribution header field"));
11331  }
11332  // Check for "world" (omit header field if found)
11333  // Attention: Overloaded and both prototypes different than in C!
11334  else if(NULL == std::strstr(p, "world"))
11335  {
11336  if(replaceHeaderField("Distribution", p))
11337  {
11338  compHeader->insert(start, "\n");
11339  compHeader->insert(start, p);
11340  compHeader->insert(start, "Distribution: ");
11341  }
11342  }
11343  else { deleteHeaderField("Distribution"); }
11344  std::free((void*) p);
11345  }
11346  }
11347  else { deleteHeaderField("Distribution"); }
11348  // Expires
11349  if(!rv)
11350  {
11351  expires = expireField->value();
11352  if(NULL != expires)
11353  {
11354  if(std::strlen(expires))
11355  {
11356  if(enc_convert_iso8601_to_timestamp(&q, expires))
11357  {
11358  SC("Do not use non-ASCII for the translation of this item")
11359  fl_message_title(S("Error"));
11360  fl_alert("%s", S("Invalid date for Expires header field"));
11361  rv = -1;
11362  }
11363  else
11364  {
11365  if(replaceHeaderField("Expires", q))
11366  {
11367  compHeader->insert(start, "\n");
11368  compHeader->insert(start, q);
11369  compHeader->insert(start, "Expires: ");
11370  }
11371  std::free((void*) q);
11372  }
11373  }
11374  }
11375  else { deleteHeaderField("Expires"); }
11376  }
11377  // Keywords
11378  if(!rv)
11379  {
11380  q = keywordField->value();
11381  if(NULL != q)
11382  {
11383  len = std::strlen(q);
11384  if(len)
11385  {
11386  // Check content
11387  for(i = 0; i < len; ++i)
11388  {
11389  // Accept a comma separated list of phrase subsets
11390  if(',' == q[i] || '!' == q[i] || '#' == q[i] || '$' == q[i]
11391  || '%' == q[i] || '&' == q[i] || '+' == q[i]
11392  || '-' == q[i] || '/' == q[i] || '=' == q[i]
11393  || '?' == q[i] || '^' == q[i] || '_' == q[i]
11394  || '{' == q[i] || '|' == q[i] || '}' == q[i]
11395  || '~' == q[i] || ' ' == q[i])
11396  {
11397  continue;
11398  }
11399  if(0x30 <= q[i] && 0x39 >= q[i]) { continue; } // 0-9
11400  if(0x41 <= q[i] && 0x5A >= q[i]) { continue; } // A-Z
11401  if(0x61 <= q[i] && 0x7A >= q[i]) { continue; } // a-z
11402  rv = -1;
11403  break;
11404  }
11405  // Reject comma or space as first and last character
11406  --len;
11407  if(',' == q[0] || ',' == q[len]
11408  || ' ' == q[0] || ' ' == q[len])
11409  {
11410  rv = -1;
11411  }
11412  if(rv)
11413  {
11414  SC("Do not use non-ASCII for the translation of this item")
11415  fl_message_title(S("Error"));
11416  fl_alert("%s",
11417  S("Invalid content in Keywords header field"));
11418  }
11419  else
11420  {
11421  if(replaceHeaderField("Keywords", q))
11422  {
11423  compHeader->insert(start, "\n");
11424  compHeader->insert(start, q);
11425  compHeader->insert(start, "Keywords: ");
11426  }
11427  }
11428  }
11429  }
11430  else { deleteHeaderField("Keywords"); }
11431  }
11432  // Followup-To
11433  if(!rv)
11434  {
11435  q = fup2Field->value();
11436  if(NULL != q)
11437  {
11438  len = std::strlen(q);
11439  if(len)
11440  {
11441  // Check content according to RFC 5536
11442  for(i = 0; i < len; ++i)
11443  {
11444  if(',' == q[i] || '.' == q[i]
11445  || '+' == q[i] || '-' == q[i] || '_' == q[i])
11446  {
11447  continue;
11448  }
11449  if(0x30 <= q[i] && 0x39 >= q[i]) { continue; }
11450  if(0x41 <= q[i] && 0x5A >= q[i]) { continue; }
11451  if(0x61 <= q[i] && 0x7A >= q[i]) { continue; }
11452  rv = -1;
11453  break;
11454  }
11455  if(',' == q[0] || ',' == q[len - (std::size_t) 1])
11456  {
11457  rv = -1;
11458  }
11459  if('.' == q[0] || '.' == q[len - (std::size_t) 1])
11460  {
11461  rv = -1;
11462  }
11463  if(rv)
11464  {
11465  SC("Do not use non-ASCII for the translation of this item")
11466  fl_message_title(S("Error"));
11467  fl_alert("%s",
11468  S("Invalid content in Followup-To header field"));
11469  }
11470  else
11471  {
11472  if(replaceHeaderField("Followup-To", q))
11473  {
11474  compHeader->insert(start, "\n");
11475  compHeader->insert(start, q);
11476  compHeader->insert(start, "Followup-To: ");
11477  fup2_present = true;
11478  }
11479  }
11480  }
11481  }
11482  else { deleteHeaderField("Followup-To"); }
11483  }
11484  }
11485  }
11486 
11487  // Generate new "Message-ID" header field for every posting attempt
11488  if(!rv)
11489  {
11490  msgid = extractHeaderField("Message-ID");
11491  if(NULL != msgid)
11492  {
11493  std::free((void*) msgid);
11494  if(std::strlen(fqdn))
11495  {
11496  msgid = core_get_msgid(fqdn);
11497  if(NULL != msgid)
11498  {
11499  replaceHeaderField("Message-ID", msgid);
11500  // Because the Cancel-Keys are based on the Message-ID,
11501  // a potential Cancel-Lock header field must be regenerated
11502  rv = searchHeaderField("Cancel-Lock", &start, &end);
11503  if(rv) { rv = 0; }
11504  else
11505  {
11506  deleteHeaderField("Cancel-Lock");
11507  // Create new Cancel-Lock header field
11508  cancel_lock1 = core_get_cancel_lock(CORE_CL_SHA1, msgid);
11509  cancel_lock = core_get_cancel_lock(CORE_CL_SHA256, msgid);
11510  if(NULL != cancel_lock1 || NULL != cancel_lock)
11511  {
11512  compHeader->insert(start, "\n");
11513  if(NULL != cancel_lock)
11514  {
11515  compHeader->insert(start, cancel_lock);
11516  compHeader->insert(start, " ");
11517  }
11518  if(NULL != cancel_lock1)
11519  {
11520  compHeader->insert(start, cancel_lock1);
11521  compHeader->insert(start, " ");
11522  }
11523  compHeader->insert(start, "Cancel-Lock:");
11524  core_free((void*) cancel_lock);
11525  core_free((void*) cancel_lock1);
11526  }
11527  }
11528  core_free((void*) msgid);
11529  }
11530  }
11531  }
11532  }
11533 
11534  // Update header field "Date" (if already present)
11535  if(!rv)
11536  {
11537  date = extractHeaderField("Date");
11538  if(NULL != date)
11539  {
11540  std::free((void*) date);
11541  date = core_get_datetime(0);
11542  rv = replaceHeaderField("Date", date);
11543  core_free((void*) date);
11544  if(rv)
11545  {
11546  SC("Do not use non-ASCII for the translation of this item")
11547  fl_message_title(S("Error"));
11548  fl_alert("%s", S("Updating Date header field failed"));
11549  rv = -1;
11550  }
11551  }
11552  }
11553 
11554  // Update header field "Injection-Date" (if already present)
11555  if(!rv)
11556  {
11557  injection_date = extractHeaderField("Injection-Date");
11558  if(NULL != injection_date)
11559  {
11560  std::free((void*) injection_date);
11561  injection_date = core_get_datetime(1);
11562  rv = replaceHeaderField("Injection-Date", injection_date);
11563  core_free((void*) injection_date);
11564  if(rv)
11565  {
11566  SC("Do not use non-ASCII for the translation of this item")
11567  fl_message_title(S("Error"));
11568  fl_alert("%s", S("Updating Injection-Date header field failed"));
11569  rv = -1;
11570  }
11571  }
11572  }
11573 
11574  // Check for Xpost (and display warning if no Fup2 is present)
11575  if(!rv)
11576  {
11577  // Attention: Overloaded and both prototypes different than in C!
11578  if(std::strchr(newsgroupsField->value(), (int) ',') && !fup2_present)
11579  {
11580  SC("Do not use non-ASCII for the translation of this item")
11581  fl_message_title(S("Warning"));
11582  rv = !fl_choice("%s", S("Cancel"),
11583  S("OK"), NULL,
11584  S("Xpost without Followup-To\nReally continue?"));
11585  }
11586  }
11587 
11588  // Combine header and body
11589  if(!rv)
11590  {
11591  header = compHeader->text();
11592  body = compText->text();
11593  if(NULL == header || NULL == body)
11594  {
11595  SC("Do not use non-ASCII for the translation of this item")
11596  fl_message_title(S("Error"));
11597  fl_alert("%s", S("Out of memory"));
11598  }
11599  else
11600  {
11601  lenh = std::strlen(header);
11602  lenb = std::strlen(body);
11603  // Check for empty body
11604  if(!lenb)
11605  {
11606  SC("Do not use non-ASCII for the translation of this item")
11607  fl_message_title(S("Error"));
11608  fl_alert("%s", S("Message body is empty"));
11609  }
11610  else
11611  {
11612  // Check body content to contain only citations
11613  if(checkArticleBody(body))
11614  {
11615  SC("Do not use non-ASCII for the translation of this item")
11616  fl_message_title(S("Error"));
11617  rv = !fl_choice("%s", S("Cancel"),
11618  S("OK"), NULL,
11619  S("Message body contains no own content"));
11620  }
11621  if(!rv)
11622  {
11623  // Post article
11624  p = (char*) std::malloc(lenh + lenb + (std::size_t) 1);
11625  if(NULL != p)
11626  {
11627  std::strcpy(p, header);
11628  std::strcat(p, body);
11629  // 'articlePost' will take responsibility for the memory
11630  mainWindow->articlePost(UI_CB_START, p);
11631  }
11632  }
11633  }
11634  std::free((void*) header);
11635  std::free((void*) body);
11636  // For code review: Do not 'free()' memory pointed to by 'p' here!
11637  }
11638  }
11639 }
11640 
11641 
11642 // =============================================================================
11643 // URI insertion callback
11644 
11645 void ComposeWindow::uri_insert_cb_i(Fl_Widget* w)
11646 {
11647  const char* scheme = uriSchemeField->value();
11648  enum enc_uri_scheme sch = ENC_URI_SCHEME_INVALID;
11649  const char* body_raw = uriBodyField->value();
11650  const char* body = NULL;
11651 
11652  // Select scheme
11653  if(std::strlen(scheme))
11654  {
11655  if(!std::strncmp(scheme, "http", 4))
11656  {
11657  sch = ENC_URI_SCHEME_HTTP;
11658  }
11659  else if(!std::strncmp(scheme, "ftp", 3))
11660  {
11661  sch = ENC_URI_SCHEME_FTP;
11662  }
11663  else if(!std::strncmp(scheme, "news", 4))
11664  {
11665  sch = ENC_URI_SCHEME_NEWS;
11666  }
11667  else if(!std::strncmp(scheme, "mailto", 6))
11668  {
11669  sch = ENC_URI_SCHEME_MAILTO;
11670  }
11671  }
11672  // Encode body
11673  body = enc_uri_percent_encode(body_raw, sch);
11674  if(NULL == body)
11675  {
11676  SC("Do not use non-ASCII for the translation of this item")
11677  fl_message_title(S("Error"));
11678  fl_alert("%s", S("Creation of URI failed"));
11679  }
11680  else
11681  {
11682  // Leading delimiter
11683  compEditor->insert("<");
11684  // Scheme
11685  compEditor->insert(scheme);
11686  // Colon delimiter
11687  compEditor->insert(":");
11688  // Authority designator
11689  if(ENC_URI_SCHEME_HTTP == sch || ENC_URI_SCHEME_FTP == sch)
11690  {
11691  compEditor->insert("//");
11692  }
11693  // Content (hier-part)
11694  compEditor->insert(body);
11695  uriBodyField->value("");
11696  // Trailing delimiter
11697  compEditor->insert(">");
11698  // Switch to content tab
11699  compTabs->value(compGroup);
11700  Fl::focus(compEditor);
11701  }
11702  if(body != body_raw) { enc_free((void*) body); }
11703 }
11704 
11705 
11706 // =============================================================================
11707 // Compose window constructor
11708 
11709 ComposeWindow::ComposeWindow(const char* label, const char* header,
11710  const char* article, const char* ca, struct core_article_header* hdr,
11711  bool super) : UI_WINDOW_CLASS(795, 395, label)
11712 {
11713  static const char label_ch[] = "1-----------------------------------------"
11714  "-------------------------72->|-78->|";
11715  Fl_Group* gp;
11716  Fl_Box* bp;
11717  Fl_Button* p;
11718  Fl_Box* chp;
11719  int th = 30; // Tab hight (including gap)
11720  int tg = 5; // Gap between tabs and cards
11721  int y, w, h, gy, gh, bw, sh, hh;
11722  int w1, w2, w3, w4, h1, h2, h3;
11723  const char* signature;
11724  const char* subject;
11725  const char* newsgroups; // Unfolded header field body
11726  const char** groups = NULL; // Array of individual group names
11727  int testGrp = 0;
11728  std::size_t i;
11729  unsigned int signature_warnings = 0;
11730  const char* uri_header;
11731  const char* distribution = NULL;
11732  int dist_msg_flag = 0;
11733  int rv;
11734  // Label strings
11735  SC("Preserve the spaces at beginning and end")
11736  const char* label_content = S(" Content ");
11737  const char* label_uri_encoder = S(" URI encoder ");
11738  const char* label_advanced = S(" Advanced ");
11739  SC("Preserve the colon")
11740  const char* label_subject = S("Subject:");
11741  const char* label_change = S("Change");
11742  const char* label_send = S("Send");
11743  const char* label_cancel = S("Cancel");
11744  const char* label_insert = S("Insert");
11745 
11746  // Configure article compose window
11747  copy_label(label);
11748  callback(cancel_cb, (void*) this);
11749 
11750  // Create text buffers
11751  compHeader = new Fl_Text_Buffer();
11752  compHeader->text(header);
11753  currentStyle = new Fl_Text_Buffer();
11754  compText = new Fl_Text_Buffer();
11755  compText->add_modify_callback(style_update_cb, (void*) this);
11756 
11757  // Add widgets --------------------------------------------------------------
11758  begin();
11759 
11760  // Create widget group
11761  compTabs = new Fl_Tabs(0, 0, 795, 395);
11762  compTabs->begin();
11763  {
11764  compGroup = new Fl_Group(0, th - tg, 795, 395 - th + tg, label_content);
11765  compGroup->begin();
11766  {
11767  // Calculate required widths and heights for buttons
11768  gui_set_default_font();
11769  fl_measure(label_send, w1 = 0, h1 = 0);
11770  fl_measure(label_cancel, w2 = 0, h2 = 0);
11771  if(w1 > w2) { w = w1; } else { w = w2; }
11772  if(h1 > h2) { h = h1; } else { h = h2; }
11773  w += 30;
11774  h += 10;
11775  gh = h + 10;
11776  gy = 395 - th - gh;
11777  // Add subject line
11778  sh = 30;
11779  subjectGroup = new Fl_Group(0, th, 795, sh);
11780  subjectGroup->begin();
11781  {
11782  gui_set_default_font();
11783  fl_measure(label_subject, w3 = 0, h3 = 0);
11784  w3 += 15;
11785  fl_measure(label_change, w4 = 0, h3 = 0);
11786  w4 += 30;
11787  subjectField = new Fl_Input(w3, th, 795 - w3 - w4, sh,
11788  label_subject);
11789  subjectField->align(FL_ALIGN_LEFT);
11790  subject = extractHeaderField("Subject");
11791  if(NULL != subject)
11792  {
11793  subjectField->value(subject);
11794  std::free((void*) subject);
11795  }
11796  p = new Fl_Button(795 - w4, th, w4, sh, label_change);
11797  p->tooltip(
11798  S("Change subject and cite old one with was: prefix in parenthesis")
11799  );
11800  p->callback(change_cb, (void*) this);
11801  }
11802  subjectGroup->end();
11803  subjectGroup->resizable(subjectField);
11804  // Add column hints
11805  hh = sh;
11806  chp = new Fl_Box(0, th + sh, 795, hh, label_ch);
11807  chp->labelfont(FL_COURIER);
11808  chp->align(FL_ALIGN_INSIDE | FL_ALIGN_LEFT);
11809  // Add text editor field
11810  compEditor = new Fl_Text_Editor(0, th + sh + hh, 795,
11811  395 - th - sh - hh - gh);
11812  compEditor->textfont(FL_COURIER);
11813  compEditor->show_insert_position();
11814  resizable(compEditor);
11815  // Create content and style buffers
11816  styles = new Fl_Text_Display::Style_Table_Entry[3];
11817  styles[0].color = FL_FOREGROUND_COLOR;
11818  styles[0].font = FL_COURIER;
11819  styles[0].size = compEditor->textsize();
11820  styles[0].attr = 0;
11821  styles[1].color = FL_COLOR_CUBE + (Fl_Color) 35;
11822  styles[1].font = FL_COURIER;
11823  styles[1].size = compEditor->textsize();
11824  styles[1].attr = 0;
11825  styles[2].color = FL_RED;
11826  styles[2].font = FL_COURIER;
11827  styles[2].size = compEditor->textsize();
11828  styles[2].attr = 0;
11829  compEditor->highlight_data(currentStyle, styles, 3, 'A', NULL, NULL);
11830  // Insert content
11831  compEditor->buffer(compText);
11832  if(NULL != article)
11833  {
11834  compText->insert(0, article);
11835  // Cite article if not superseding it
11836  if(!super)
11837  {
11838  if (NULL != ca) { gui_cite_content(compText, ca, hdr->groups); }
11839  }
11840  gui_check_article(compText);
11841  }
11842  if(super) { compEditor->insert_position(0); }
11843  else { compEditor->insert_position(compText->length()); }
11844  // Append signature (if available)
11845  signature = core_get_signature(&signature_warnings);
11846  if(NULL != signature)
11847  {
11848  if(!(signature_warnings & CORE_SIG_FLAG_INVALID))
11849  {
11850  if(!super) { compEditor->buffer()->append("\n"); }
11851  if(signature_warnings & CORE_SIG_FLAG_SEPARATOR)
11852  {
11853  compEditor->buffer()->append("-- \n");
11854  }
11855  compEditor->buffer()->append(signature);
11856  }
11857  core_free((void*) signature);
11858  }
11859  // Add button bar at bottom
11860  gp = new Fl_Group(0, th + gy, 795, gh);
11861  gp->begin();
11862  {
11863  bw = w + 30;
11864  y = th + gy + 5;
11865  new Fl_Box(0, th + gy, bw, gh);
11866  p = new Fl_Button(15, y, w, h, label_send);
11867  p->callback(send_cb, (void*) this);
11868  new Fl_Box(795 - bw, gy, bw, gh);
11869  p = new Fl_Button(795 - bw + 15, y, w, h, label_cancel);
11870  p->callback(cancel_cb, (void*) this);
11871  // Resizable space between buttons
11872  bp = new Fl_Box(bw, th + gy, 795 - (2 * bw), gh);
11873  }
11874  gp->end();
11875  gp->resizable(bp);
11876  }
11877  compGroup->end();
11878  compGroup->resizable(compEditor);
11879 
11880  // -----------------------------------------------------------------------
11881 
11882  uriEncGroup = new Fl_Group(0, th - tg, 795, 395 - th + tg,
11883  label_uri_encoder);
11884  uriEncGroup->begin();
11885  {
11886  y = th + tg + 10;
11887  uri_header = S("Percent encoder for clickable URIs");
11888  uriHeaderField = new Fl_Box(5, y, 795 - 10, sh, uri_header);
11889  uriHeaderField->labelfont(FL_HELVETICA_BOLD);
11890  y += 2 * sh;
11891  uriSchemeField = new Fl_Input_Choice(5, y, 795 - 10, sh,
11892  S("URI scheme:"));
11893  uriSchemeField->tooltip(S("Only the selectable schemes are supported"));
11894  uriSchemeField->align(FL_ALIGN_TOP_LEFT);
11895  uriSchemeField->add("http");
11896  uriSchemeField->add("https");
11897  uriSchemeField->add("ftp");
11898  uriSchemeField->add("news");
11899  uriSchemeField->add("mailto");
11900  uriSchemeField->input()->readonly(1);
11901  uriSchemeField->value(0); // Default to first entry in option list
11902  y += 2 * sh;
11903  uriBodyField = new Fl_Input(5, y, 795 - 10, sh, S("Body for URI:"));
11904  uriBodyField->align(FL_ALIGN_TOP_LEFT);
11905  // Add insert button at bottom
11906  y += 2 * sh;
11907  fl_measure(label_insert, w1 = 0, h1 = 0);
11908  w = w1 + 30;
11909  h = h1 + 10;
11910  gp = new Fl_Group(0, y, 795, h);
11911  gp->begin();
11912  {
11913  p = new Fl_Button(5, y, w, h, label_insert);
11914  p->callback(uri_insert_cb, (void*) this);
11915  fillSpace = new Fl_Box(5 + w, y, 795 - (10 + w), h);
11916  }
11917  gp->end();
11918  gp->resizable(fillSpace);
11919  // Fill unused space below button
11920  y += sh + 5;
11921  fillSpace = new Fl_Box(5, y, 795 - 10, 1);
11922  }
11923  uriEncGroup->end();
11924  uriEncGroup->resizable(fillSpace);
11925 
11926  // -----------------------------------------------------------------------
11927 
11928  advancedGroup = new Fl_Group(0, th - tg, 795, 395 - th + tg,
11929  label_advanced);
11930  advancedGroup->begin();
11931  {
11932  y = th + tg + 20;
11933  newsgroupsField = new Fl_Input(5, y, 795 - 10, sh, "Newsgroups:");
11934  newsgroupsField->align(FL_ALIGN_TOP_LEFT);
11935  newsgroupsField->tooltip(S("Comma separated list"));
11936  newsgroups = extractHeaderField("Newsgroups");
11937  if(NULL != newsgroups)
11938  {
11939  newsgroupsField->value(newsgroups);
11940  // Split body into group array and check for test groups
11941  groups = core_extract_groups(newsgroups);
11942  if(NULL != groups)
11943  {
11944  // Check group array (and destroy it again)
11945  i = 0;
11946  while(NULL != groups[i])
11947  {
11948  if(!testGrp) { testGrp = filter_check_testgroup(groups[i]); }
11949  core_free((void*) groups[i++]);
11950  }
11951  core_free((void*) groups);
11952  }
11953  // Release memory for header field body
11954  std::free((void*) newsgroups);
11955  }
11956  y += 2 * sh;
11957  fup2Field = new Fl_Input_Choice(5, y, 795 - 10, sh, "Followup-To:");
11958  fup2Field->align(FL_ALIGN_TOP_LEFT);
11959  fup2Field->tooltip(S("Comma separated list"));
11960  fup2Field->add("poster");
11961  if(NULL != hdr)
11962  {
11963  for(i = 0; i < UI_XPOST_LIMIT; ++i)
11964  {
11965  if(NULL == hdr->groups[i]) { break; }
11966  else
11967  {
11968  // Check for Xpost
11969  if(!i && NULL == hdr->groups[1]) { break; }
11970  fup2Field->add(hdr->groups[i]);
11971  }
11972  }
11973  if(super)
11974  {
11975  if(NULL != hdr->fup2 && std::strlen(hdr->fup2))
11976  {
11977  fup2Field->value(hdr->fup2);
11978  }
11979  }
11980  }
11981  y += 2 * sh;
11982  keywordField = new Fl_Input_Choice(5, y, 795 - 10, sh, "Keywords:");
11983  keywordField->align(FL_ALIGN_TOP_LEFT);
11984  keywordField->tooltip(S("Comma separated list"));
11985  if(!testGrp) { keywordField->add("ignore"); }
11986  else
11987  {
11988  // Set keywords for test group
11989  std::printf("%s: %s"
11990  "Setting keywords from config file for test group\n",
11991  CFG_NAME, MAIN_ERR_PREFIX);
11992  keywordField->value(config[CONF_TESTGRP_KWORDS].val.s);
11993  }
11994  y += 2 * sh;
11995  expireField = new Fl_Input(5, y, 795 - 10, sh, "Expires:");
11996  expireField->align(FL_ALIGN_TOP_LEFT);
11997  expireField->tooltip(
11998  S("Use date in ISO 8601 format YYYY-MM-DD")
11999  );
12000  y += 2 * sh;
12001  distriField = new Fl_Input_Choice(5, y, 795 - 10, sh, "Distribution:");
12002  distriField->align(FL_ALIGN_TOP_LEFT);
12003  if(NULL != hdr && NULL != hdr->dist && std::strlen(hdr->dist))
12004  {
12005  // Follow distribution for follow-up
12006  if(!super)
12007  {
12008  std::printf("%s: %sFollowing former distribution\n",
12009  CFG_NAME, MAIN_ERR_PREFIX);
12010  }
12011  distriField->value(hdr->dist);
12012  }
12013  else
12014  {
12015  // Follow suggestion of server
12016  rv = -1;
12017  newsgroups = extractHeaderField("Newsgroups");
12018  if(NULL != newsgroups)
12019  {
12020  // Split newsgroup field body into group array
12021  groups = core_extract_groups(newsgroups);
12022  // Match groups against distribution patterns
12023  rv = core_get_distribution(&distribution, groups);
12024  if(NULL != groups)
12025  {
12026  // Destroy group array again
12027  i = 0;
12028  while(NULL != groups[i]) { core_free((void*) groups[i++]); }
12029  core_free((void*) groups);
12030  }
12031  // Release memory for header field body
12032  std::free((void*) newsgroups);
12033  }
12034  if(!rv && config[CONF_DIST_SUGG].val.i)
12035  {
12036  dist_msg_flag = 1;
12037  std::printf("%s: %sSetting distribution as suggested\n",
12038  CFG_NAME, MAIN_ERR_PREFIX);
12039  distriField->value(distribution);
12040  core_free((void*) distribution);
12041  }
12042  else { distriField->value("world"); }
12043  }
12044  distriField->add("world");
12045  distriField->add("local");
12046  distriField->tooltip(
12047  S("Use country code or comma separated list of country codes")
12048  );
12049  y += sh + 5;
12050  archiveButton = new Fl_Check_Button(5, y, 795 - 10, sh, "Archive");
12051  archiveButton->set();
12052  // Fill unused space after resize
12053  y += sh + 5;
12054  fillSpace = new Fl_Box(5, y, 795 - 10, 1);
12055  }
12056  advancedGroup->end();
12057  advancedGroup->resizable(fillSpace);
12058  }
12059  compTabs->end();
12060  compTabs->resizable(compGroup);
12061 
12062  end();
12063  // --------------------------------------------------------------------------
12064 
12065  // Set focus
12066  if(!std::strlen(subjectField->value())) { Fl::focus(subjectField); }
12067  else { Fl::focus(compEditor); }
12068  // Set minimum window size
12069  size_range(4 * bw, 395, 0, 0);
12070 
12071  // Display potential message for distribution
12072  if(dist_msg_flag)
12073  {
12074  SC("Do not use non-ASCII for the translation of this item")
12075  fl_message_title(S("Note"));
12076  fl_message("%s", S("Distribution set as suggested by server"));
12077  }
12078 
12079  // Display potential warnings for signature
12080  if(signature_warnings & CORE_SIG_FLAG_INVALID)
12081  {
12082  SC("Do not use non-ASCII for the translation of this item")
12083  fl_message_title(S("Error"));
12084  fl_alert("%s", S("Signature has unsupported character set"));
12085  }
12086  else
12087  {
12088  if(signature_warnings & CORE_SIG_FLAG_LENGTH)
12089  {
12090  SC("Do not use non-ASCII for the translation of this item")
12091  fl_message_title(S("Note"));
12092  fl_message("%s", S("Signature should not be longer than 4 lines"));
12093  }
12094  }
12095 
12096  // Check for external editor
12097  if(!std::strlen(config[CONF_EDITOR].val.s))
12098  {
12099 #if !CFG_DB_DISABLE
12100  Fl::visual(FL_DOUBLE | FL_INDEX);
12101 #endif // CFG_DB_DISABLE
12102  show();
12103  }
12104  else
12105  {
12106  // Since FLTK 1.3.3 it is required to explicitly hide the new window
12107  hide();
12108  mainWindow->composeWindowLock = 1;
12109  UI_STATUS(S("Waiting for external editor ..."));
12110  }
12111 }
12112 
12113 
12114 // =============================================================================
12115 // Compose window destructor
12116 
12117 ComposeWindow::~ComposeWindow(void)
12118 {
12119  // Detach text buffers and destroy them
12120  compEditor->highlight_data(dummyTb, NULL, 0, 'A', NULL, NULL);
12121  delete currentStyle;
12122  delete[] styles;
12123  compEditor->buffer(dummyTb);
12124  delete compText;
12125  delete compHeader;
12126 }
12127 
12128 
12129 // =============================================================================
12130 //! \brief Init GUI
12131 //!
12132 //! \attention
12133 //! Remember that the locale must use either UTF-8 or ISO 8859-1 codeset or be
12134 //! the POSIX locale.
12135 //!
12136 //! \param[in] argc Command line argument count
12137 //! \param[in] argv Pointer to Command line argument array
12138 
12139 void ui_init(int argc, char** argv)
12140 {
12141  int rv;
12142  int w = 730;
12143  int h = 350;
12144  int x = 1;
12145  int y = 1;
12146  int tx = 230;
12147  int ty = 140;
12148  const char* text;
12149  Fl_Text_Buffer* info;
12150  const char* pass = NULL;
12151  const char* title_psfile;
12152 
12153  // Initialize MT support of FLTK
12154  rv = Fl::lock();
12155  if(rv)
12156  {
12157  PRINT_ERROR("No thread support available in FLTK");
12158  exitRequest = 1;
12159  }
12160  else
12161  {
12162  // This flag must be set after MT support of FLTK was initialized,
12163  // but before spawning any additional threads!
12164  lockingInitialized = true;
12165  // Start core (this will spawn a second thread)
12166  rv = core_init();
12167  if(rv) { exitRequest = 1; }
12168  }
12169  if(!exitRequest)
12170  {
12171  // Set global button labels
12172  SC("")
12173  SC("This section is for the button labels of popup windows")
12174  fl_cancel = S("Cancel");
12175  fl_close = S("Close");
12176  fl_ok = S("OK");
12177  fl_yes = S("Yes");
12178  fl_no = S("No");
12179  SC("")
12180 
12181  // Set global labels for text buffer
12182  SC("")
12183  SC("This section is for the text buffer warning messages")
12184  Fl_Text_Buffer::file_encoding_warning_message
12185  = S("Invalid encoding detected, trying to convert data");
12186 
12187  // Set global labels for file chooser
12188  SC("")
12189  SC("This section is for the file chooser labels")
12190  Fl_File_Chooser::all_files_label = S("*");
12191  Fl_File_Chooser::custom_filter_label = S("Custom filter");
12192  Fl_File_Chooser::existing_file_label
12193  = S("Please choose an existing file!");
12194  Fl_File_Chooser::favorites_label = S("Favorites");
12195  Fl_File_Chooser::add_favorites_label = S("Add to favorites");
12196  Fl_File_Chooser::manage_favorites_label = S("Manage favorites");
12197  Fl_File_Chooser::filesystems_label = S("File systems");
12198  Fl_File_Chooser::new_directory_label = S("New directory?");
12199  Fl_File_Chooser::new_directory_tooltip = S("Create a new directory.");
12200  Fl_File_Chooser::preview_label = S("Preview");
12201  Fl_File_Chooser::hidden_label = S("Show hidden files");
12202  Fl_File_Chooser::filename_label = S("Filename:");
12203  Fl_File_Chooser::save_label = S("Save");
12204  SC("The translation for this should not be much longer!")
12205  Fl_File_Chooser::show_label = S("Show:");
12206  SC("")
12207 
12208  // Set global labels for print dialog
12209  SC("")
12210  SC("This section is for the print dialog labels")
12211  SC("Do not use characters for the translation that cannot be")
12212  SC("converted to the ISO 8859-1 character set for this item.")
12213  SC("Leave the original string in place if in doubt.")
12214  Fl_Printer::dialog_title = S("Print");
12215  Fl_Printer::dialog_printer = S("Printer");
12216  Fl_Printer::dialog_range = S("Print range");
12217  Fl_Printer::dialog_copies = S("Copies");
12218  Fl_Printer::dialog_all = S("All");
12219  Fl_Printer::dialog_pages = S("Pages");
12220  Fl_Printer::dialog_from = S("From:");
12221  Fl_Printer::dialog_to = S("To:");
12222  Fl_Printer::dialog_properties = S("Properties ...");
12223  Fl_Printer::dialog_copyNo = S("Copies:");
12224  Fl_Printer::dialog_print_button = S("Print");
12225  Fl_Printer::dialog_cancel_button = S("Cancel");
12226  Fl_Printer::dialog_print_to_file = S("Print to file");
12227  SC("")
12228 
12229  // Set global labels for printer property dialog
12230  SC("")
12231  SC("This section is for the printer property dialog labels")
12232  SC("Do not use characters for the translation that cannot be")
12233  SC("converted to the ISO 8859-1 character set for this item.")
12234  SC("Leave the original string in place if in doubt.")
12235  Fl_Printer::property_title = S("Printer properties");
12236  Fl_Printer::property_pagesize = S("Page size:");
12237  Fl_Printer::property_mode = S("Output mode:");
12238  Fl_Printer::property_use = S("Use");
12239  Fl_Printer::property_save = S("Save");
12240  Fl_Printer::property_cancel = S("Cancel");
12241  SC("")
12242 
12243  // Set global label for print to file chooser dialog
12244  SC("")
12245  SC("This section is for the print to file chooser dialog label")
12246  SC("Do not use characters for the translation that cannot be")
12247  SC("converted to the ISO 8859-1 character set for this item.")
12248  SC("Leave the original string in place if in doubt.")
12249  title_psfile = S("Select a .ps file");
12250  SC("")
12251  // Following the FLTK documentation this should always work
12252  Fl_PostScript_File_Device::file_chooser_title = title_psfile;
12253 
12254  // Check clamp article count value
12255  if(UI_CAC_MIN > config[CONF_CAC].val.i)
12256  {
12257  config[CONF_CAC].val.i = UI_CAC_MIN;
12258  }
12259  if(UI_CAC_MAX < config[CONF_CAC].val.i)
12260  {
12261  config[CONF_CAC].val.i = UI_CAC_MAX;
12262  }
12263 
12264 #if CFG_CMPR_DISABLE
12265  // Disable compression negotiation if compiled without support
12267 #endif // CFG_CMPR_DISABLE
12268 
12269  // Construct main window
12270  dummyTb = new Fl_Text_Buffer(0, 0);
12271  mainWindow = new MainWindow(CFG_NAME);
12272 
12273  // Restore last window position and size
12274  mainWindow->size_range(w, h, 0, 0);
12275  if(config[CONF_POS_X].val.i >= x) { x = config[CONF_POS_X].val.i; }
12276  if(config[CONF_POS_Y].val.i >= y) { y = config[CONF_POS_Y].val.i; }
12277  if(config[CONF_SIZE_X].val.i >= w) { w = config[CONF_SIZE_X].val.i; }
12278  if(config[CONF_SIZE_Y].val.i >= h) { h = config[CONF_SIZE_Y].val.i; }
12279  mainWindow->resize(x, y, w, h);
12280  // Restore tiling
12281  tx = config[CONF_TILE_X].val.i;
12282  ty = config[CONF_TILE_Y].val.i;
12283  mainWindow->setTilingX(tx);
12284  mainWindow->setTilingY(ty);
12285  mainWindow->redraw();
12286 
12287  // Don't move message/choice windows under mouse cursor
12288  fl_message_hotspot(0);
12289 
12290 #if USE_WINDOW_ICON
12291  // Set default icon
12292  mainWindow->default_icon(&mainIcon);
12293 #endif // USE_WINDOW_ICON
12294 
12295  // Show GUI
12296  // The command line argument '-display' is parsed here. Ensure that the
12297  // connection to the X server is not opened before this point!
12298 #if !CFG_DB_DISABLE
12299  Fl::visual(FL_DOUBLE | FL_INDEX);
12300 #endif // CFG_DB_DISABLE
12301  mainWindow->show(argc, argv);
12302 
12303  // =======================================================================
12304 
12305  // Set default font
12306  gui_set_default_font();
12307 
12308  // Initialize current article
12309  info = new Fl_Text_Buffer;
12310  text = gui_greeting();
12311  info->text(text);
12312  delete[] text;
12313  mainWindow->articleUpdate(info);
12314 
12315  // Display warning if TLS module has detected potential vulnerability
12316  // Note: Do not move the NLS string inside the #if block
12317  text = S("Possible security vulnerability in TLS module detected!");
12318 #if CFG_USE_TLS
12319 # if !CFG_TLS_WARNING_DISABLE
12320  rv = tls_vulnerability_check(1);
12321 # else // !CFG_TLS_WARNING_DISABLE
12322  rv = tls_vulnerability_check(0);
12323 # endif // !CFG_TLS_WARNING_DISABLE
12324  if(0 > rv)
12325  {
12326  SC("Do not use non-ASCII for the translation of this item")
12327  fl_message_title(S("Warning"));
12328  fl_alert("%s", text);
12329  }
12330 #endif // CFG_USE_TLS
12331 
12332  // Check TLS certificate CRL update interval
12333  // Ask user whether automatic updates should be disabled if elapsed
12334 #if CFG_USE_TLS
12335  rv = tls_crl_update_check();
12336  if(rv)
12337  {
12338  SC("Do not use non-ASCII for the translation of this item")
12339  fl_message_title(S("Note"));
12340  rv = fl_choice("%s", S("Skip"),
12341  S("OK"), NULL,
12342  S("TLS certificate revocation list \x28\x43RL\x29\nupdate interval elapsed. Update now?"));
12343  if(!rv)
12344  {
12345  // Suppress automatic CRL updates for current session
12346  std::printf("%s: %sTLS certificate CRL updates suppressed\n",
12347  CFG_NAME, MAIN_ERR_PREFIX);
12349  }
12350  }
12351 #endif // CFG_USE_TLS
12352 
12353  // Ask for password if there is none but authentication is enabled
12354  if(!config[CONF_PASS].val.s[0]) { conf_ephemeral_passwd = 1; }
12355  if(1 == config[CONF_AUTH].val.i && conf_ephemeral_passwd)
12356  {
12357  do
12358  {
12359  pass = fl_password("%s", config[CONF_PASS].val.s, "Password:");
12360  if(NULL == pass) { exitRequest = 1; break; }
12361  else
12362  {
12364  if(!config[CONF_PASS].val.s[0])
12365  {
12366  SC("Do not use non-ASCII for the translation of this item")
12367  fl_message_title(S("Warning"));
12368  fl_alert("%s", S("Empty password is not supported"));
12369  }
12370  }
12371  } while(!config[CONF_PASS].val.s[0]);
12372  }
12373 
12374  // Initialize group tree via group list refresh timeout callback
12375  MainWindow::group_list_refresh_timer_cb((void*) mainWindow);
12376 
12377  //! \todo
12378  //! Workaround for "creeping window" problem without session manager seems
12379  //! not to be the "right" solution.
12380  //! If you know a better one, please report it.
12381  // Some window managers reposition the window while adding the borders.
12382  // First we assign the CPU so that this can happen.
12383  // Then we read back the new position and calculate correction offsets.
12384  // The stored correction offsets are used on shutdown to calculate the
12385  // positions that are saved in configfile.
12386  // The correction is limited to 20 points for the case that the window
12387  // manager moves the window to a completely different position.
12388  Fl::check();
12389  offset_correction_x = x - mainWindow->x();
12390  if(20 < offset_correction_x) { offset_correction_x = 0; }
12391  offset_correction_y = y - mainWindow->y();
12392  if(20 < offset_correction_y) { offset_correction_y = 0; }
12393  }
12394 }
12395 
12396 
12397 // =============================================================================
12398 //! \brief Drive GUI
12399 //!
12400 //! Assign the CPU to the GUI to process queued operations.
12401 //! Before this function is called, \ref ui_init() must be executed.
12402 //!
12403 //! \return
12404 //! - 0 on success
12405 //! - 1 to indicate an exit request from the GUI
12406 
12407 int ui_exec(void)
12408 {
12409  static const char* tmpfile = NULL;
12410  static long int editor_pid;
12411  static bool editor_term_lock = false;
12412  int res = 0;
12413  int rv;
12414  bool error;
12415 
12416  // Check whether main window is present
12417  if(NULL == mainWindow) { res = 1; }
12418  else
12419  {
12420  // Update protocol console
12421  if(NULL != protocolConsole) { protocolConsole->update(); }
12422 
12423  // Drive GUI
12424  Fl::wait(0.5);
12425 
12426  // State machine to handle child process for external editor
12427  if(NULL == mainWindow->composeWindow)
12428  {
12429  mainWindow->composeWindowLock = 0;
12430  }
12431  switch(mainWindow->composeWindowLock)
12432  {
12433  case 0:
12434  {
12435  // Check for exit request
12436  if(exitRequest) { res = 1; }
12437  break;
12438  }
12439  case 1: // Spawn child process for external editor
12440  {
12441  error = true;
12442  tmpfile = core_tmpfile_create();
12443  if(NULL != tmpfile)
12444  {
12445  rv = gui_save_to_file(tmpfile, mainWindow->composeWindow
12446  ->compEditor->buffer());
12447  if(!rv)
12448  {
12449  rv = ext_editor(tmpfile, 1, &editor_pid);
12450  if(!rv) { error = false; }
12451  }
12452  if(error)
12453  {
12454  core_tmpfile_delete(tmpfile);
12455  tmpfile = NULL;
12456  }
12457  }
12458  if(error)
12459  {
12460  UI_STATUS(S("Starting external editor failed."));
12461  mainWindow->composeWindowLock = 3;
12462  }
12463  else { mainWindow->composeWindowLock = 2; }
12464  break;
12465  }
12466  case 2: // Poll state of external editor
12467  {
12468  rv = ext_editor_status(editor_pid);
12469  if(-1 == rv)
12470  {
12471  // External editor still running
12472  if(exitRequest && !editor_term_lock)
12473  {
12474  // Terminate external editor
12475  ext_editor_terminate(editor_pid);
12476  editor_term_lock = true;
12477  }
12478  }
12479  else
12480  {
12481  // External editor child process has terminated
12482  if(rv) { UI_STATUS(S("External editor reported error.")); }
12483  else
12484  {
12485  // Import data from external editor
12486  UI_STATUS(S("External editor reported success."));
12487  rv = mainWindow->composeWindow
12488  ->compEditor->buffer()->loadfile(tmpfile);
12489  if(rv)
12490  {
12491  mainWindow->composeWindow
12492  ->compEditor->buffer()
12493  ->append(S("[Importing data from editor failed]"));
12494  }
12495  }
12496  core_tmpfile_delete(tmpfile);
12497  tmpfile = NULL;
12498  mainWindow->composeWindowLock = 3;
12499  }
12500  break;
12501  }
12502  case 3: // Show compose window
12503  {
12504  editor_term_lock = false;
12505  mainWindow->composeWindow->show();
12506  mainWindow->composeWindowLock = 0;
12507  break;
12508  }
12509  default:
12510  {
12511  PRINT_ERROR("Error in external editor state machine (bug)");
12512  res = 1;
12513  break;
12514  }
12515  }
12516  }
12517 
12518  return(res);
12519 }
12520 
12521 
12522 // =============================================================================
12523 //! \brief Shutdown GUI
12524 //!
12525 //! It is not allowed to call \ref ui_exec() after this function returns.
12526 
12527 void ui_exit(void)
12528 {
12529  if(NULL != mainWindow)
12530  {
12531  // Store current main window size
12532  config[CONF_POS_X].val.i = mainWindow->x() + offset_correction_x;
12533  config[CONF_POS_Y].val.i = mainWindow->y() + offset_correction_y;
12534  config[CONF_SIZE_X].val.i = mainWindow->w();
12535  config[CONF_SIZE_Y].val.i = mainWindow->h();
12536  config[CONF_TILE_X].val.i = mainWindow->getTilingX();
12537  config[CONF_TILE_Y].val.i = mainWindow->getTilingY();
12538 
12539  // Store group states
12540  if(main_debug)
12541  {
12542  PRINT_ERROR("Export group states for shutdown");
12543  }
12544  mainWindow->groupStateExport();
12545  }
12546 
12547  // Shutdown core (and join with the core thread)
12548  core_exit();
12549  // MT lock of FLTK no longer required
12550  lockingInitialized = false;
12551 
12552  // The main window must exist until the core was shutdown (to be able to catch
12553  // all remaining callbacks)
12554  if(NULL != mainWindow)
12555  {
12556  // Destroy main window
12557  // This must be done after core shutdown (joining the core thread),
12558  // otherwise the core thread may create callbacks to the already destroyed
12559  // object.
12560  delete mainWindow;
12561 
12562  // This must be done last because the dummy buffer is required to destroy
12563  // the main window.
12564  delete dummyTb;
12565  }
12566 
12567 }
12568 
12569 
12570 // =============================================================================
12571 //! \brief Check whether locale use UTF-8 encoding
12572 //!
12573 //! Determine which codeset the locale uses that FLTK will set for \c LC_CTYPE
12574 //! after opening the X11 display.
12575 //!
12576 //! \attention
12577 //! This works even if the X11 display is not opened yet! This means the return
12578 //! value is valid before \ref ui_init() is called too.
12579 //!
12580 //! \return
12581 //! - 1 Locale use UTF-8 encoding
12582 //! - 0 Locale use other encoding
12583 
12585 {
12586  // Up to FLTK 1.3.3, this function return the required information
12587  return(fl_utf8locale());
12588 }
12589 
12590 
12591 // =============================================================================
12592 //! \brief Wakeup callback (called by core thread after operation has finished)
12593 //!
12594 //! \param[in] cookie Cookie that was assigned to the operation by GUI
12595 
12596 void ui_wakeup(unsigned int cookie)
12597 {
12598  int rv;
12599 
12600  // Schedule the callback indicated by 'cookie' and awake UI thread
12601  switch(cookie)
12602  {
12603  case UI_CB_COOKIE_SERVER:
12604  {
12605  rv = Fl::awake(MainWindow::serverconf_cb, (void*) mainWindow);
12606  break;
12607  }
12608  case UI_CB_COOKIE_GROUPLIST:
12609  {
12610  rv = Fl::awake(MainWindow::subscribe_cb1, (void*) mainWindow);
12611  break;
12612  }
12613  case UI_CB_COOKIE_GROUPLABELS:
12614  {
12615  rv = Fl::awake(MainWindow::subscribe_cb2, (void*) mainWindow);
12616  break;
12617  }
12618  case UI_CB_COOKIE_GROUPPROPOSAL:
12619  {
12620  rv = Fl::awake(MainWindow::groupproposal_cb, (void*) mainWindow);
12621  break;
12622  }
12623  case UI_CB_COOKIE_GROUPINFO1:
12624  {
12625  rv = Fl::awake(MainWindow::refresh_cb1, (void*) mainWindow);
12626  break;
12627  }
12628  case UI_CB_COOKIE_GROUPINFO2:
12629  {
12630  rv = Fl::awake(MainWindow::refresh_cb2, (void*) mainWindow);
12631  break;
12632  }
12633  case UI_CB_COOKIE_GROUP:
12634  {
12635  rv = Fl::awake(MainWindow::group_cb, (void*) mainWindow);
12636  break;
12637  }
12638  case UI_CB_COOKIE_OVERVIEW:
12639  {
12640  rv = Fl::awake(MainWindow::overview_cb, (void*) mainWindow);
12641  break;
12642  }
12643  case UI_CB_COOKIE_HEADER:
12644  {
12645  rv = Fl::awake(MainWindow::header_cb, (void*) mainWindow);
12646  break;
12647  }
12648  case UI_CB_COOKIE_BODY:
12649  {
12650  rv = Fl::awake(MainWindow::body_cb, (void*) mainWindow);
12651  break;
12652  }
12653  case UI_CB_COOKIE_MOTD:
12654  {
12655  rv = Fl::awake(MainWindow::motd_cb, (void*) mainWindow);
12656  break;
12657  }
12658  case UI_CB_COOKIE_ARTICLE:
12659  {
12660  rv = Fl::awake(MainWindow::article_cb, (void*) mainWindow);
12661  break;
12662  }
12663  case UI_CB_COOKIE_SRC:
12664  {
12665  rv = Fl::awake(MainWindow::src_cb, (void*) mainWindow);
12666  break;
12667  }
12668  case UI_CB_COOKIE_POST:
12669  {
12670  rv = Fl::awake(MainWindow::post_cb, (void*) mainWindow);
12671  break;
12672  }
12673  default:
12674  {
12675  PRINT_ERROR("Can't assign cookie to callback function");
12676  rv = -1;
12677  break;
12678  }
12679  }
12680  if(rv)
12681  {
12682  PRINT_ERROR("Registering awake callback failed (fatal error)");
12683  exitRequest = 1;
12684  }
12685 }
12686 
12687 
12688 // =============================================================================
12689 //! \brief Lock for multithread support
12690 //!
12691 //! This function must be called by other threads before they call an OS
12692 //! function that is not guaranteed to be thread-safe. Prominent examples are:
12693 //! - \c getenv()
12694 //! - \c gethostbyname()
12695 //! - \c localtime()
12696 //! - \c readdir()
12697 //!
12698 //! FLTK documentation for MT locking:
12699 //! <br>
12700 //! http://www.fltk.org/doc-1.3/advanced.html#advanced_multithreading
12701 //!
12702 //! \return
12703 //! - 0 on success
12704 //! - Negative value on error
12705 
12706 int ui_lock(void)
12707 {
12708  int res = 0;
12709 
12710  // Only the UI thread exist until locking is initialized
12711  if(lockingInitialized)
12712  {
12713  res = Fl::lock();
12714  if(res) { res = -1; }
12715  }
12716 
12717  return(res);
12718 }
12719 
12720 
12721 // =============================================================================
12722 //! \brief Unlock for multithread support
12723 //!
12724 //! \return
12725 //! - 0 on success
12726 //! - Negative value on error
12727 
12728 int ui_unlock(void)
12729 {
12730  // Only the UI thread exist until locking is initialized
12731  if(lockingInitialized)
12732  {
12733  Fl::unlock();
12734  }
12735 
12736  return(0);
12737 }
12738 
12739 
12740 //! @}
12741 
12742 // EOF
core_mutex_lock
void core_mutex_lock(void)
Lock mutex for data object (exported for UI)
Definition: core.c:6562
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:5411
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:3555
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:1859
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:5116
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:200
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:6249
filter_get_score
int filter_get_score(const struct core_hierarchy_element *he)
Get article score.
Definition: filter.c:1677
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:4966
filter_match_own
int filter_match_own(const struct core_hierarchy_element *he)
Check for own article.
Definition: filter.c:1587
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:3317
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:8868
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:12596
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:86
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:5788
tls_crl_update_check
int tls_crl_update_check(void)
Check whether CRL update interval has elapsed.
Definition: tls.c:2838
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:8402
UI_BUSY
#define UI_BUSY()
Display "Busy" in main window progress bar.
Definition: gui.cxx:1650
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:4986
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:7856
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:4103
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:7717
enc_rot13
void enc_rot13(char *data)
Encode or decode data with ROT13 algorithm.
Definition: encoding.c:4692
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:5377
core_hierarchy_element::child
struct core_hierarchy_element ** child
Definition: core.h:149
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:7536
core_reset_group_states
int core_reset_group_states(unsigned int cookie)
Reset states of subscribed groups (exported for UI)
Definition: core.c:3512
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:4604
core_get_distribution
int core_get_distribution(const char **dist, const char **groups)
Get distribution suggestions (exported for UI)
Definition: core.c:3885
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:5049
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:3468
core_free
void core_free(void *p)
Free an object allocated by core (exported for UI)
Definition: core.c:6625
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:5924
conf_entry_val::s
char * s
Definition: conf.h:103
UI_HDR_BUFSIZE
#define UI_HDR_BUFSIZE
Size of buffer for header field creation.
Definition: gui.cxx:188
enc_percent_decode
int enc_percent_decode(char *s, int clean)
Percent decoder.
Definition: encoding.c:8318
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:4766
core_post_article
int core_post_article(const char *article, unsigned int cookie)
Post article (exported for UI)
Definition: core.c:4495
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:6641
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:912
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:6699
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:85
ui_unlock
int ui_unlock(void)
Unlock for multithread support.
Definition: gui.cxx:12728
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:4119
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:3685
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:6530
core_sort_group_list
int core_sort_group_list(void)
Alphabetically sort group list (exported for UI)
Definition: core.c:3416
UI_COLOR_RADIO_BUTTON
#define UI_COLOR_RADIO_BUTTON
Color for selected radio button.
Definition: gui.cxx:134
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:3822
core_subscribe_group
int core_subscribe_group(const char *name)
Store group subscription (exported for UI)
Definition: core.c:3436
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:4342
core_mutex_unlock
void core_mutex_unlock(void)
Unlock mutex for data object (exported for UI)
Definition: core.c:6578
enc_extract_addr_spec
const char * enc_extract_addr_spec(const char *mailbox)
Extract addr-spec token from RFC 5322 mailbox.
Definition: encoding.c:4830
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:194
CONF_COLOR_LEVEL3
Definition: conf.h:87
core_get_group_labels
int core_get_group_labels(unsigned int cookie)
Get list of newsgroup labels (exported for UI)
Definition: core.c:3376
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:5022
UI_PROGRESS
#define UI_PROGRESS(s, e)
Update value of main window progress bar.
Definition: gui.cxx:1663
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:5629
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:3732
conf_entry_val::i
int i
Definition: conf.h:102
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:3641
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:1638
ui_exec
int ui_exec(void)
Drive GUI.
Definition: gui.cxx:12407
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:132
ext_editor_status
int ext_editor_status(long int editor_pid)
Poll status of external editor.
Definition: extutils.c:603
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:8168
ext_pp_filter
const char * ext_pp_filter(const char *article)
External post processing filter for outgoing articles.
Definition: extutils.c:682
CONF_COMPRESSION
Definition: conf.h:72
filter_check_testgroup
int filter_check_testgroup(const char *group)
Check for test group.
Definition: filter.c:1527
core_check_file_exist
int core_check_file_exist(const char *pathname)
Check wheter file exists (exported for UI)
Definition: core.c:6340
UI_STYLES_LEN
#define UI_STYLES_LEN
Number of styles for article content syntax highlighting.
Definition: gui.cxx:138
MAIN_ERR_PREFIX
#define MAIN_ERR_PREFIX
Message prefix for MAIN module.
Definition: gui.cxx:123
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:12584
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:5162
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:7801
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:4558
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:4871
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:958
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:12706
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:5485
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:4664
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:5359
CONF_USER
Definition: conf.h:43
xdg_get_confdir
const char * xdg_get_confdir(const char *)
Get configuration directory.
Definition: xdg.c:115
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:4421
UI_STATUS
#define UI_STATUS(s)
Replace message in main window status bar.
Definition: gui.cxx:1635
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:88
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:5763
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:5695
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:4235
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:4741
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:4503
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:6034
ENC_CTS_ALTERNATIVE
Definition: encoding.h:41
CONF_INV_ORDER
Definition: conf.h:69
conf::val
union conf_entry_val val
Definition: conf.h:111
enc_uc_repair_utf8
const char * enc_uc_repair_utf8(const char *s)
Repair UTF-8 encoding.
Definition: encoding.c:5181
ext_editor_terminate
void ext_editor_terminate(long int editor_pid)
Terminate external editor.
Definition: extutils.c:649
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:6415
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:4166
CONF_QUOTEUNIFY
Definition: conf.h:60
main
int main(int argc, char **argv)
Program entry point.
Definition: main.cxx:315
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:3952
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:7619
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:4359
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:3581
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:3284
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:6543
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:191
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:1635
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:4297
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:8210
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:6103
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:7235
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:5295
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:6370
tls_crl_update_control
void tls_crl_update_control(int)
Enable or disable automatic CRL updates.
Definition: tls.c:2869
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:6152
ui_exit
void ui_exit(void)
Shutdown GUI.
Definition: gui.cxx:12527
ext_free
void ext_free(void *p)
Free an object allocated by external program delegation module.
Definition: extutils.c:1143
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:12139
core_set_group
int core_set_group(const char *name, unsigned int cookie)
Set current group (exported for UI)
Definition: core.c:3763

Generated at 2024-04-27 using  doxygen