1 /*
2  *  Project   : tin - a Usenet reader
3  *  Module    : filter.c
4  *  Author    : I. Lea
5  *  Created   : 1992-12-28
6  *  Updated   : 2020-05-28
7  *  Notes     : Filter articles. Kill & auto selection are supported.
8  *
9  * Copyright (c) 1991-2021 Iain Lea <iain@bricbrac.de>
10  * All rights reserved.
11  *
12  * Redistribution and use in source and binary forms, with or without
13  * modification, are permitted provided that the following conditions
14  * are met:
15  *
16  * 1. Redistributions of source code must retain the above copyright notice,
17  *    this list of conditions and the following disclaimer.
18  *
19  * 2. Redistributions in binary form must reproduce the above copyright
20  *    notice, this list of conditions and the following disclaimer in the
21  *    documentation and/or other materials provided with the distribution.
22  *
23  * 3. Neither the name of the copyright holder nor the names of its
24  *    contributors may be used to endorse or promote products derived from
25  *    this software without specific prior written permission.
26  *
27  * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
29  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
31  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
32  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
34  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
35  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
37  * POSSIBILITY OF SUCH DAMAGE.
38  */
39 
40 
41 #ifndef TIN_H
42 #	include "tin.h"
43 #endif /* !TIN_H */
44 #ifndef VERSION_H
45 #	include "version.h"
46 #endif /* !VERSION_H */
47 #ifndef TCURSES_H
48 #	include "tcurses.h"
49 #endif /* !TCURSES_H */
50 
51 
52 #define IS_READ(i)	(arts[i].status == ART_READ)
53 #define IS_KILLED(i)	(arts[i].killed)
54 #define IS_KILLED_UNREAD(i)	(arts[i].killed == ART_KILLED_UNREAD)
55 #define IS_SELECTED(i)	(arts[i].selected)
56 
57 /*
58  * SET_FILTER in group grp, current article arts[i], with rule ptr[j]
59  *
60  * filtering is now done this way:
61  * a. set score for all articles and rules
62  * b. check each article if the score is above or below the limit
63  *
64  * SET_FILTER is now somewhat shorter, as the real filtering is done
65  * at the end of filter_articles()
66  */
67 
68 #define SET_FILTER(grp, i, j)	\
69 	if (ptr[j].score > 0) { \
70 		arts[i].score = (SCORE_MAX - ptr[j].score >= arts[i].score) ? \
71 		(arts[i].score + ptr[j].score) : SCORE_MAX ; \
72 	} else { \
73 		arts[i].score = (-SCORE_MAX - ptr[j].score <= arts[i].score) ? \
74 		(arts[i].score + ptr[j].score) : -SCORE_MAX ; }
75 
76 /*
77  * These will probably go away when filtering is rewritten
78  * Easier access to hashed msgids. Note that in REFS(), y must be free()d
79  * msgid is mandatory in an article and cannot be NULL
80  */
81 #define MSGID(x)			(x->refptr ? x->refptr->txt : "")
82 #define REFS(x,y)			((y = get_references(x->refptr ? x->refptr->parent : NULL)) ? y : "")
83 
84 /*
85  * global filter array
86  */
87 struct t_filters glob_filter = { 0, 0, (struct t_filter *) 0 };
88 
89 
90 /*
91  * Global filter file offset
92  */
93 int filter_file_offset;
94 
95 
96 /*
97  * Local prototypes
98  */
99 static int get_choice(int x, const char *help, const char *prompt, char *list[], int list_size);
100 static int set_filter_scope(struct t_group *group);
101 static struct t_filter_comment *add_filter_comment(struct t_filter_comment *ptr, char *text);
102 static struct t_filter_comment *free_filter_comment(struct t_filter_comment *ptr);
103 static struct t_filter_comment *copy_filter_comment(struct t_filter_comment *from, struct t_filter_comment *to);
104 static t_bool add_filter_rule(struct t_group *group, struct t_article *art, struct t_filter_rule *rule, t_bool quick_filter_rule);
105 static int test_regex(const char *string, char *regex, t_bool nocase, struct regex_cache *cache);
106 static void expand_filter_array(struct t_filters *ptr);
107 static void fmt_filter_menu_prompt(char *dest, size_t dest_len, const char *fmt_str, int len, const char *text);
108 static void free_filter_item(struct t_filter *ptr);
109 static void print_filter_menu(void);
110 static void set_filter(struct t_filter *ptr);
111 static void write_filter_array(FILE *fp, struct t_filters *ptr);
112 #if 0 /* currently unused */
113 	static FILE *open_xhdr_fp(char *header, long min, long max);
114 #endif /* 0 */
115 
116 
117 /*
118  * Add one more entry to the filter-comment-list.
119  * If ptr == NULL the list will be created.
120  */
121 static struct t_filter_comment *
add_filter_comment(struct t_filter_comment * ptr,char * text)122 add_filter_comment(
123 	struct t_filter_comment *ptr,
124 	char *text)
125 {
126 	if (ptr == NULL) {
127 		ptr = my_malloc(sizeof(struct t_filter_comment));
128 		ptr->text = my_strdup(text);
129 		ptr->next = (struct t_filter_comment *) 0;
130 	} else
131 		ptr->next = add_filter_comment(ptr->next, text);
132 
133 	return ptr;
134 }
135 
136 
137 /*
138  * Free all entries in a filter-comment-list.
139  * Set ptr to NULL and return it.
140  */
141 static struct t_filter_comment *
free_filter_comment(struct t_filter_comment * ptr)142 free_filter_comment(
143 	struct t_filter_comment *ptr)
144 {
145 	struct t_filter_comment *tmp, *next;
146 
147 	tmp = ptr;
148 	while (tmp != NULL) {
149 		next = tmp->next;
150 		free(tmp->text);
151 		free(tmp);
152 		tmp = next;
153 	}
154 
155 	return tmp;
156 }
157 
158 
159 /*
160  * Copy the filter-comment-list 'from' into the list 'to'.
161  */
162 static struct t_filter_comment *
copy_filter_comment(struct t_filter_comment * from,struct t_filter_comment * to)163 copy_filter_comment(
164 	struct t_filter_comment *from,
165 	struct t_filter_comment *to)
166 {
167 	if (from != NULL) {
168 		to = my_malloc(sizeof(struct t_filter_comment));
169 		to->text = my_strdup(from->text);
170 		to->next = copy_filter_comment(from->next, NULL);
171 	}
172 
173 	return to;
174 }
175 
176 
177 static void
expand_filter_array(struct t_filters * ptr)178 expand_filter_array(
179 	struct t_filters *ptr)
180 {
181 	int num;
182 	size_t block;
183 
184 	num = ++ptr->max;
185 
186 	block = num * sizeof(struct t_filter);
187 
188 	if (num == 1)	/* allocate */
189 		ptr->filter = my_malloc(block);
190 	else	/* reallocate */
191 		ptr->filter = my_realloc(ptr->filter, block);
192 }
193 
194 
195 /*
196  * Looks for a matching filter hit (wildmat or pcre regex) in the supplied string
197  * If the cache is not yet initialised, compile and optimise the regex
198  * Returns 1 if we hit the rule
199  * Returns 0 if we had no match
200  * In case of error prints an error message and returns -1
201  */
202 static int
test_regex(const char * string,char * regex,t_bool nocase,struct regex_cache * cache)203 test_regex(
204 	const char *string,
205 	char *regex,
206 	t_bool nocase,
207 	struct regex_cache *cache)
208 {
209 	int regex_errpos;
210 
211 	if (!tinrc.wildcard) {
212 		if (wildmat(string, regex, nocase))
213 			return 1;
214 	} else {
215 		if (!cache->re)
216 			compile_regex(regex, cache, (nocase ? PCRE_CASELESS : 0));
217 		if (cache->re) {
218 			regex_errpos = pcre_exec(cache->re, cache->extra, string, strlen(string), 0, 0, NULL, 0);
219 			if (regex_errpos >= 0)
220 				return 1;
221 			else if (regex_errpos != PCRE_ERROR_NOMATCH) { /* also exclude PCRE_ERROR_BADUTF8 ? */
222 				error_message(2, _(txt_pcre_error_num), regex_errpos);
223 #ifdef DEBUG
224 				if (debug & DEBUG_FILTER) {
225 					debug_print_file("FILTER", _(txt_pcre_error_num), regex_errpos);
226 					debug_print_file("FILTER", "\t regex: %s", regex);
227 					debug_print_file("FILTER", "\tstring: %s", string);
228 				}
229 #endif /* DEBUG */
230 				return -1;
231 			}
232 		}
233 	}
234 	return 0;
235 }
236 
237 
238 /*
239  * set_filter() initialises a struct t_filter with default values
240  */
241 static void
set_filter(struct t_filter * ptr)242 set_filter(
243 	struct t_filter *ptr)
244 {
245 	if (ptr != NULL) {
246 		ptr->comment = (struct t_filter_comment *) 0;
247 		ptr->scope = NULL;
248 		ptr->inscope = TRUE;
249 		ptr->icase = FALSE;
250 		ptr->fullref = FILTER_MSGID;
251 		ptr->subj = NULL;
252 		ptr->from = NULL;
253 		ptr->msgid = NULL;
254 		ptr->lines_cmp = FILTER_LINES_NO;
255 		ptr->lines_num = 0;
256 		ptr->gnksa_cmp = FILTER_LINES_NO;
257 		ptr->gnksa_num = 0;
258 		ptr->score = 0;
259 		ptr->xref = NULL;
260 		ptr->path = NULL;
261 		ptr->time = (time_t) 0;
262 		ptr->next = (struct t_filter *) 0;
263 	}
264 }
265 
266 
267 /*
268  * free_filter_item() frees all filter data (char *)
269  */
270 static void
free_filter_item(struct t_filter * ptr)271 free_filter_item(
272 	struct t_filter *ptr)
273 {
274 	ptr->comment = free_filter_comment(ptr->comment);
275 	FreeAndNull(ptr->scope);
276 	FreeAndNull(ptr->subj);
277 	FreeAndNull(ptr->from);
278 	FreeAndNull(ptr->msgid);
279 	FreeAndNull(ptr->xref);
280 	FreeAndNull(ptr->path);
281 }
282 
283 
284 /*
285  * free_filter_array() frees t_filter structs t_filters contains pointers to
286  */
287 void
free_filter_array(struct t_filters * ptr)288 free_filter_array(
289 	struct t_filters *ptr)
290 {
291 	int i;
292 
293 	if (ptr != NULL) {
294 		for (i = 0; i < ptr->num; i++)
295 			free_filter_item(ptr->filter + i);
296 
297 		FreeAndNull(ptr->filter);
298 		ptr->num = 0;
299 		ptr->max = 0;
300 	}
301 }
302 
303 
304 /*
305  * read ~/.tin/filter file contents into filter array
306  */
307 t_bool
read_filter_file(const char * file)308 read_filter_file(
309 	const char *file)
310 {
311 	FILE *fp;
312 	char buf[HEADER_LEN];
313 	char scope[HEADER_LEN];
314 	char comment_line[LEN];	/* one line of comment */
315 	char subj[HEADER_LEN];
316 	char from[HEADER_LEN];
317 	char msgid[HEADER_LEN];
318 	char buffer[HEADER_LEN];
319 	char gnksa[HEADER_LEN];
320 	char xref[HEADER_LEN];
321 	char path[HEADER_LEN];
322 	char scbuf[PATH_LEN];
323 	int i = 0;
324 	int icase = 0;
325 	int score = 0;
326 	long secs = 0L;
327 	struct t_filter_comment *comment = NULL;
328 	struct t_filter *ptr = NULL;
329 	t_bool expired = FALSE;
330 	t_bool expired_time = FALSE;
331 	time_t current_secs = (time_t) 0;
332 	static t_bool first_read = TRUE;
333 	struct t_version *upgrade = NULL;
334 
335 	if ((fp = fopen(file, "r")) == NULL)
336 		return FALSE;
337 
338 	if (!batch_mode || verbose)
339 		wait_message(0, _(txt_reading_filter_file));
340 
341 	(void) time(&current_secs);
342 
343 	/*
344 	 * Reset all filter arrays if doing a reread of the active file
345 	 */
346 	if (!first_read)
347 		free_filter_array(&glob_filter);
348 
349 	filter_file_offset = 1;
350 	scope[0] = '\0';
351 	while (fgets(buf, (int) sizeof(buf), fp) != NULL) {
352 		if (*buf == '\n')
353 			continue;
354 		if (*buf == '#') {
355 			if (scope[0] == '\0')
356 				filter_file_offset++;
357 			if (upgrade == NULL && first_read && match_string(buf, "# Filter file V", NULL, 0)) {
358 				first_read = FALSE;
359 				upgrade = check_upgrade(buf, "# Filter file V", FILTER_VERSION);
360 				if (upgrade->state != RC_IGNORE)
361 					upgrade_prompt_quit(upgrade, FILTER_FILE); /* TODO: do something (more) useful here */
362 			}
363 			continue;
364 		}
365 
366 		switch (my_tolower((unsigned char) buf[0])) {
367 			case 'c':
368 				if (match_integer(buf + 1, "ase=", &icase, 1)) {
369 					if (ptr && !expired_time)
370 						ptr[i].icase = (unsigned) icase;
371 
372 					break;
373 				}
374 				if (match_string(buf + 1, "omment=", comment_line, sizeof(comment_line))) {
375 					str_trim(comment_line);
376 					comment = add_filter_comment(comment, comment_line);
377 				}
378 				break;
379 
380 			case 'f':
381 				if (match_string(buf + 1, "rom=", from, sizeof(from))) {
382 					if (ptr && !expired_time) {
383 						if (tinrc.wildcard && ptr[i].from != NULL) {
384 							/* merge with already read value */
385 							ptr[i].from = my_realloc(ptr[i].from, strlen(ptr[i].from) + strlen(from) + 2);
386 							strcat(ptr[i].from, "|");
387 							strcat(ptr[i].from, from);
388 						} else {
389 							FreeIfNeeded(ptr[i].from);
390 							ptr[i].from = my_strdup(from);
391 						}
392 					}
393 				}
394 				break;
395 
396 			case 'g':
397 				if (match_string(buf + 1, "roup=", scope, sizeof(scope))) {
398 					str_trim(scope);
399 #ifdef DEBUG
400 					if (debug & DEBUG_FILTER)
401 						debug_print_file("FILTER", "\nnum=[%d] group=[%s]", glob_filter.num, scope);
402 #endif /* DEBUG */
403 					if (glob_filter.num >= glob_filter.max)
404 						expand_filter_array(&glob_filter);
405 
406 					ptr = glob_filter.filter;
407 					i = glob_filter.num++;
408 					set_filter(&ptr[i]);
409 					expired_time = FALSE;
410 					ptr[i].scope = my_strdup(scope);
411 					if (comment != NULL) {
412 						ptr[i].comment = copy_filter_comment(comment, ptr[i].comment);
413 						comment = free_filter_comment(comment);
414 					}
415 					subj[0] = '\0';
416 					from[0] = '\0';
417 					msgid[0] = '\0';
418 					buffer[0] = '\0';
419 					xref[0] = '\0';
420 					path[0] = '\0';
421 					icase = 0;
422 					secs = 0L;
423 					break;
424 				}
425 				if (match_string(buf + 1, "nksa=", gnksa, sizeof(gnksa))) {
426 					if (ptr && !expired_time) {
427 						if (gnksa[0] == '<') {
428 							ptr[i].gnksa_cmp = FILTER_LINES_LT;
429 							ptr[i].gnksa_num = atoi(&gnksa[1]);
430 						} else if (gnksa[0] == '>') {
431 							ptr[i].gnksa_cmp = FILTER_LINES_GT;
432 							ptr[i].gnksa_num = atoi(&gnksa[1]);
433 						} else {
434 							ptr[i].gnksa_cmp = FILTER_LINES_EQ;
435 							ptr[i].gnksa_num = atoi(gnksa);
436 						}
437 					}
438 				}
439 				break;
440 
441 			case 'l':
442 				if (match_string(buf + 1, "ines=", buffer, sizeof(buffer))) {
443 					if (ptr && !expired_time) {
444 						if (buffer[0] == '<') {
445 							ptr[i].lines_cmp = FILTER_LINES_LT;
446 							ptr[i].lines_num = atoi(&buffer[1]);
447 						} else if (buffer[0] == '>') {
448 							ptr[i].lines_cmp = FILTER_LINES_GT;
449 							ptr[i].lines_num = atoi(&buffer[1]);
450 						} else {
451 							ptr[i].lines_cmp = FILTER_LINES_EQ;
452 							ptr[i].lines_num = atoi(buffer);
453 						}
454 					}
455 				}
456 				break;
457 
458 			case 'm':
459 				if (match_string(buf + 1, "sgid=", msgid, sizeof(msgid))) {
460 					if (ptr && !expired_time) {
461 						if (tinrc.wildcard && ptr[i].msgid != NULL && ptr[i].fullref == FILTER_MSGID) {
462 							/* merge with already read value */
463 							ptr[i].msgid = my_realloc(ptr[i].msgid, strlen(ptr[i].msgid) + strlen(msgid) + 2);
464 							strcat(ptr[i].msgid, "|");
465 							strcat(ptr[i].msgid, msgid);
466 						} else {
467 							FreeIfNeeded(ptr[i].msgid);
468 							ptr[i].msgid = my_strdup(msgid);
469 							ptr[i].fullref = FILTER_MSGID;
470 						}
471 					}
472 					break;
473 				}
474 				if (match_string(buf + 1, "sgid_last=", msgid, sizeof(msgid))) {
475 					if (ptr && !expired_time) {
476 						if (tinrc.wildcard && ptr[i].msgid != NULL && ptr[i].fullref == FILTER_MSGID_LAST) {
477 							/* merge with already read value */
478 							ptr[i].msgid = my_realloc(ptr[i].msgid, strlen(ptr[i].msgid) + strlen(msgid) + 2);
479 							strcat(ptr[i].msgid, "|");
480 							strcat(ptr[i].msgid, msgid);
481 						} else {
482 							FreeIfNeeded(ptr[i].msgid);
483 							ptr[i].msgid = my_strdup(msgid);
484 							ptr[i].fullref = FILTER_MSGID_LAST;
485 						}
486 					}
487 					break;
488 				}
489 				if (match_string(buf + 1, "sgid_only=", msgid, sizeof(msgid))) {
490 					if (ptr && !expired_time) {
491 						if (tinrc.wildcard && ptr[i].msgid != NULL && ptr[i].fullref == FILTER_MSGID_ONLY) {
492 							/* merge with already read value */
493 							ptr[i].msgid = my_realloc(ptr[i].msgid, strlen(ptr[i].msgid) + strlen(msgid) + 2);
494 							strcat(ptr[i].msgid, "|");
495 							strcat(ptr[i].msgid, msgid);
496 						} else {
497 							FreeIfNeeded(ptr[i].msgid);
498 							ptr[i].msgid = my_strdup(msgid);
499 							ptr[i].fullref = FILTER_MSGID_ONLY;
500 						}
501 					}
502 				}
503 				break;
504 
505 			case 'p':
506 				if (match_string(buf + 1, "ath=", path, sizeof(path))) {
507 					str_trim(path);
508 					if (ptr && !expired_time) {
509 						if (tinrc.wildcard && ptr[i].path != NULL) {
510 							/* merge with already read value */
511 							ptr[i].path = my_realloc(ptr[i].path, strlen(ptr[i].path) + strlen(path) + 2);
512 							strcat(ptr[i].path, "|");
513 							strcat(ptr[i].path, path);
514 						} else {
515 							FreeIfNeeded(ptr[i].path);
516 							ptr[i].path = my_strdup(path);
517 						}
518 					}
519 				}
520 				break;
521 
522 			case 'r':
523 				if (match_string(buf + 1, "efs_only=", msgid, sizeof(msgid))) {
524 					if (ptr && !expired_time) {
525 						if (tinrc.wildcard && ptr[i].msgid != NULL && ptr[i].fullref == FILTER_REFS_ONLY) {
526 							/* merge with already read value */
527 							ptr[i].msgid = my_realloc(ptr[i].msgid, strlen(ptr[i].msgid) + strlen(msgid) + 2);
528 							strcat(ptr[i].msgid, "|");
529 							strcat(ptr[i].msgid, msgid);
530 						} else {
531 							FreeIfNeeded(ptr[i].msgid);
532 							ptr[i].msgid = my_strdup(msgid);
533 							ptr[i].fullref = FILTER_REFS_ONLY;
534 						}
535 					}
536 				}
537 				break;
538 
539 			case 's':
540 				if (match_string(buf + 1, "ubj=", subj, sizeof(subj))) {
541 					if (ptr && !expired_time) {
542 						if (tinrc.wildcard && ptr[i].subj != NULL) {
543 							/* merge with already read value */
544 							ptr[i].subj = my_realloc(ptr[i].subj, strlen(ptr[i].subj) + strlen(subj) + 2);
545 							strcat(ptr[i].subj, "|");
546 							strcat(ptr[i].subj, subj);
547 						} else {
548 							FreeIfNeeded(ptr[i].subj);
549 							ptr[i].subj = my_strdup(subj);
550 						}
551 #ifdef DEBUG
552 						if (debug & DEBUG_FILTER)
553 							debug_print_file("FILTER", "buf=[%s]  Gsubj=[%s]", ptr[i].subj, glob_filter.filter[i].subj);
554 #endif /* DEBUG */
555 					}
556 					break;
557 				}
558 
559 				/*
560 				 * read score for rule
561 				 */
562 				if (match_string(buf + 1, "core=", scbuf, PATH_LEN)) {
563 					score = atoi(scbuf);
564 #ifdef DEBUG
565 					if (debug & DEBUG_FILTER)
566 						debug_print_file("FILTER", "score=[%d]", score);
567 #endif /* DEBUG */
568 					if (ptr && !expired_time) {
569 						if (score > SCORE_MAX)
570 							score = SCORE_MAX;
571 						else {
572 							if (score < -SCORE_MAX)
573 								score = -SCORE_MAX;
574 							else {
575 								if (!score) {
576 									if (!strncasecmp(scbuf, "kill", 4))
577 										score = tinrc.score_kill;
578 									else {
579 										if (!strncasecmp(scbuf, "hot", 3))
580 											score = tinrc.score_select;
581 									}
582 								}
583 							}
584 						}
585 						ptr[i].score = score;
586 					}
587 				}
588 				break;
589 
590 			case 't':
591 				if (match_long(buf + 1, "ime=", &secs)) {
592 					if (ptr && !expired_time) {
593 						ptr[i].time = (time_t) secs;
594 						/* rule expired? */
595 						if (secs && current_secs > (time_t) secs) {
596 #ifdef DEBUG
597 							if (debug & DEBUG_FILTER)
598 								debug_print_file("FILTER", "EXPIRED  secs=[%lu]  current_secs=[%lu]", (unsigned long int) secs, (unsigned long int) current_secs);
599 #endif /* DEBUG */
600 							glob_filter.num--;
601 							expired_time = TRUE;
602 							expired = TRUE;
603 						}
604 					}
605 				}
606 				break;
607 
608 			case 'x':
609 				/*
610 				 * TODO: format has changed in FILTER_VERSION 1.0.0,
611 				 *       should we comment out older xref rules like below?
612 				 */
613 				if (match_string(buf + 1, "ref=", xref, sizeof(xref))) {
614 					str_trim(xref);
615 					if (ptr && !expired_time) {
616 						if (tinrc.wildcard && ptr[i].xref != NULL) {
617 							/* merge with already read value */
618 							ptr[i].xref = my_realloc(ptr[i].xref, strlen(ptr[i].xref) + strlen(xref) + 2);
619 							strcat(ptr[i].xref, "|");
620 							strcat(ptr[i].xref, xref);
621 						} else {
622 							FreeIfNeeded(ptr[i].xref);
623 							ptr[i].xref = my_strdup(xref);
624 						}
625 					}
626 					break;
627 				}
628 				if (upgrade && upgrade->state == RC_UPGRADE) {
629 					char foo[HEADER_LEN];
630 
631 					if (match_string(buf + 1, "ref_max=", foo, LEN - 1)) {
632 						/*
633 						 * TODO: add to the right rule, give better explanation.
634 						 */
635 						snprintf(foo, HEADER_LEN, "%s%s", _(txt_removed_rule), str_trim(buf));
636 						comment = add_filter_comment(comment, foo);
637 						break;
638 					}
639 					if (match_string(buf + 1, "ref_score=", foo, LEN - 1)) {
640 						/*
641 						 * TODO: add to the right rule, give better explanation.
642 						 */
643 						snprintf(foo, HEADER_LEN, "%s%s", _(txt_removed_rule), str_trim(buf));
644 						comment = add_filter_comment(comment, foo);
645 					}
646 				}
647 				break;
648 
649 			default:
650 				break;
651 		}
652 	}
653 
654 	if (comment) /* stray comment without scope */
655 		(void) free_filter_comment(comment);
656 
657 	fclose(fp);
658 
659 	if (expired || (upgrade && upgrade->state == RC_UPGRADE))
660 		write_filter_file(file);
661 
662 	if (!cmd_line && !batch_mode)
663 		clear_message();
664 
665 	FreeAndNull(upgrade);
666 	return TRUE;
667 }
668 
669 
670 /*
671  * write filter strings to ~/.tin/filter
672  */
673 void
write_filter_file(const char * filename)674 write_filter_file(
675 	const char *filename)
676 {
677 	FILE *fp;
678 	char *file_tmp;
679 	int i;
680 	long fpos;
681 
682 	if (no_write)
683 		return;
684 
685 	/* generate tmp-filename */
686 	file_tmp = get_tmpfilename(filename);
687 
688 	if (!backup_file(filename, file_tmp)) {
689 		error_message(2, _(txt_filesystem_full_backup), filename);
690 		free(file_tmp);
691 		return;
692 	}
693 
694 	if ((fp = fopen(filename, "w+")) == NULL) {
695 		free(file_tmp);
696 		return;
697 	}
698 
699 	/* TODO: -> lang.c */
700 	fprintf(fp, "# Filter file V%s for the TIN newsreader\n#\n", FILTER_VERSION);
701 	fprintf(fp, "%s", _(txt_filter_file));
702 
703 	fflush(fp);
704 
705 	/* determine the file offset */
706 	if (!batch_mode) {
707 		if ((fpos = ftell(fp)) <= 0) {
708 			clearerr(fp);
709 			fclose(fp);
710 			rename_file(file_tmp, filename);
711 			free(file_tmp);
712 			error_message(2, _(txt_filesystem_full), filename);
713 			return;
714 		}
715 		rewind(fp);
716 		filter_file_offset = 1;
717 		while ((i = fgetc(fp)) != EOF) {
718 			if (i == '\n')
719 				filter_file_offset++;
720 		}
721 		if (fseek(fp, fpos, SEEK_SET)) {
722 			clearerr(fp);
723 			fclose(fp);
724 			rename_file(file_tmp, filename);
725 			free(file_tmp);
726 			error_message(2, _(txt_filesystem_full), filename);
727 			return;
728 		}
729 	}
730 
731 	/*
732 	 * Save global filters
733 	 */
734 	write_filter_array(fp, &glob_filter);
735 
736 	if ((i = ferror(fp)) || fclose(fp)) {
737 		error_message(2, _(txt_filesystem_full), filename);
738 		rename_file(file_tmp, filename);
739 		if (i) {
740 			clearerr(fp);
741 			fclose(fp);
742 		}
743 	} else
744 		unlink(file_tmp);
745 
746 	free(file_tmp);
747 }
748 
749 
750 static void
write_filter_array(FILE * fp,struct t_filters * ptr)751 write_filter_array(
752 	FILE *fp,
753 	struct t_filters *ptr)
754 {
755 	int i;
756 	struct t_filter_comment *comment;
757 	time_t theTime = time(NULL);
758 
759 	if (ptr == NULL)
760 		return;
761 
762 	for (i = 0; i < ptr->num; i++) {
763 #ifdef DEBUG
764 		if (debug & DEBUG_FILTER)
765 			debug_print_file("FILTER", "WRITE i=[%d] subj=[%s] from=[%s]\n", i, BlankIfNull(ptr->filter[i].subj), BlankIfNull(ptr->filter[i].from));
766 #endif /* DEBUG */
767 
768 		if (ptr->filter[i].time && theTime > ptr->filter[i].time)
769 			continue;
770 #ifdef DEBUG
771 		if (debug & DEBUG_FILTER)
772 			debug_print_file("FILTER", "Scope=[%s]" cCRLF, (ptr->filter[i].scope != NULL ? ptr->filter[i].scope : "*"));
773 #endif /* DEBUG */
774 
775 		fprintf(fp, "\n");		/* makes filter file more readable */
776 
777 		/* comments appear always first, if there are any... */
778 		if (ptr->filter[i].comment != NULL) {
779 			/*
780 			 * Save the start of the list, in case write_filter_array is
781 			 * called multiple times. Otherwise the list would get lost.
782 			 */
783 			comment = ptr->filter[i].comment;
784 			while (ptr->filter[i].comment != NULL) {
785 				fprintf(fp, "comment=%s\n", ptr->filter[i].comment->text);
786 				ptr->filter[i].comment = ptr->filter[i].comment->next;
787 			}
788 			ptr->filter[i].comment = comment;
789 		}
790 
791 		fprintf(fp, "group=%s\n", (ptr->filter[i].scope != NULL ? ptr->filter[i].scope : "*"));
792 
793 		fprintf(fp, "case=%u\n", ptr->filter[i].icase);
794 
795 		if (ptr->filter[i].score == tinrc.score_kill)
796 			fprintf(fp, "score=kill\n");
797 		else if (ptr->filter[i].score == tinrc.score_select)
798 			fprintf(fp, "score=hot\n");
799 		else
800 			fprintf(fp, "score=%d\n", ptr->filter[i].score);
801 
802 		if (ptr->filter[i].subj != NULL)
803 			fprintf(fp, "subj=%s\n", ptr->filter[i].subj);
804 
805 		if (ptr->filter[i].from != NULL)
806 			fprintf(fp, "from=%s\n", ptr->filter[i].from);
807 
808 		if (ptr->filter[i].msgid != NULL) {
809 			switch (ptr->filter[i].fullref) {
810 				case FILTER_MSGID:
811 					fprintf(fp, "msgid=%s\n", ptr->filter[i].msgid);
812 					break;
813 
814 				case FILTER_MSGID_LAST:
815 					fprintf(fp, "msgid_last=%s\n", ptr->filter[i].msgid);
816 					break;
817 
818 				case FILTER_MSGID_ONLY:
819 					fprintf(fp, "msgid_only=%s\n", ptr->filter[i].msgid);
820 					break;
821 
822 				case FILTER_REFS_ONLY:
823 					fprintf(fp, "refs_only=%s\n", ptr->filter[i].msgid);
824 					break;
825 
826 				default:
827 					break;
828 			}
829 		}
830 
831 		if (ptr->filter[i].lines_cmp != FILTER_LINES_NO) {
832 			switch (ptr->filter[i].lines_cmp) {
833 				case FILTER_LINES_EQ:
834 					fprintf(fp, "lines=%d\n", ptr->filter[i].lines_num);
835 					break;
836 
837 				case FILTER_LINES_LT:
838 					fprintf(fp, "lines=<%d\n", ptr->filter[i].lines_num);
839 					break;
840 
841 				case FILTER_LINES_GT:
842 					fprintf(fp, "lines=>%d\n", ptr->filter[i].lines_num);
843 					break;
844 
845 				default:
846 					break;
847 			}
848 		}
849 
850 		if (ptr->filter[i].gnksa_cmp != FILTER_LINES_NO) {
851 			switch (ptr->filter[i].gnksa_cmp) {
852 				case FILTER_LINES_EQ:
853 					fprintf(fp, "gnksa=%d\n", ptr->filter[i].gnksa_num);
854 					break;
855 
856 				case FILTER_LINES_LT:
857 					fprintf(fp, "gnksa=<%d\n", ptr->filter[i].gnksa_num);
858 					break;
859 
860 				case FILTER_LINES_GT:
861 					fprintf(fp, "gnksa=>%d\n", ptr->filter[i].gnksa_num);
862 					break;
863 
864 				default:
865 					break;
866 			}
867 		}
868 
869 		if (ptr->filter[i].xref != NULL)
870 			fprintf(fp, "xref=%s\n", ptr->filter[i].xref);
871 
872 		if (ptr->filter[i].path != NULL)
873 			fprintf(fp, "path=%s\n", ptr->filter[i].path);
874 
875 		if (ptr->filter[i].time) {
876 			char timestring[25];
877 			if (my_strftime(timestring, sizeof(timestring) - 1, "%Y-%m-%d %H:%M:%S UTC", gmtime(&(ptr->filter[i].time))))
878 				fprintf(fp, "time=%lu (%s)\n", (unsigned long int) ptr->filter[i].time, timestring);
879 		}
880 	}
881 	fflush(fp);
882 }
883 
884 
885 /*
886  * Interactive filter menu
887  */
888 static int
get_choice(int x,const char * help,const char * prompt,char * list[],int list_size)889 get_choice(
890 	int x,
891 	const char *help,
892 	const char *prompt,
893 	char *list[],
894 	int list_size)
895 {
896 	int ch, y, i = 0;
897 
898 	if (help)
899 		show_menu_help(help);
900 
901 	if (list == NULL || list_size < 1)
902 		return -1;
903 
904 	y = strwidth(prompt);
905 
906 	do {
907 		MoveCursor(x, y);
908 		my_fputs(list[i], stdout);
909 		my_flush();
910 		CleartoEOLN();
911 		ch = ReadCh();
912 		switch (ch) {
913 			case ' ':
914 				i++;
915 				i %= list_size;
916 				break;
917 
918 			case ESC:	/* (ESC) common arrow keys */
919 #	ifdef HAVE_KEY_PREFIX
920 			case KEY_PREFIX:
921 #	endif /* HAVE_KEY_PREFIX */
922 				switch (get_arrow_key(ch)) {
923 					case KEYMAP_UP:
924 						i--;
925 						if (i < 0)
926 							i = list_size - 1;
927 						ch = ' ';	/* don't exit the while loop yet */
928 						break;
929 
930 					case KEYMAP_DOWN:
931 						i++;
932 						i %= list_size;
933 						ch = ' ';	/* don't exit the while loop yet */
934 						break;
935 
936 					default:
937 						break;
938 				}
939 				break;
940 
941 			default:
942 				break;
943 		}
944 	} while (ch != '\n' && ch != '\r' && ch != iKeyAbort); /* TODO: replace hard coded keynames */
945 
946 	if (ch == iKeyAbort)
947 		return -1;
948 
949 	return i;
950 }
951 
952 
953 static const char *ptr_filter_comment;
954 static const char *ptr_filter_lines;
955 static const char *ptr_filter_menu;
956 static const char *ptr_filter_scope;
957 static const char *ptr_filter_text;
958 static const char *ptr_filter_time;
959 static const char *ptr_filter_groupname;
960 static char text_subj[PATH_LEN];
961 static char text_from[PATH_LEN];
962 static char text_msgid[PATH_LEN];
963 static char text_score[PATH_LEN];
964 
965 
966 static void
print_filter_menu(void)967 print_filter_menu(
968 	void)
969 {
970 	ClearScreen();
971 
972 	center_line(0, TRUE, ptr_filter_menu);
973 
974 	MoveCursor(INDEX_TOP, 0);
975 	my_printf("%s%s%s", ptr_filter_comment, cCRLF, cCRLF);
976 	my_printf("%s%s", ptr_filter_text, cCRLF);
977 	my_printf("%s%s%s", _(txt_filter_text_type), cCRLF, cCRLF);
978 	my_printf("%s%s", text_subj, cCRLF);
979 	my_printf("%s%s", text_from, cCRLF);
980 	my_printf("%s%s%s", text_msgid, cCRLF, cCRLF);
981 	my_printf("%s%s", ptr_filter_lines, cCRLF);
982 	my_printf("%s%s", text_score, cCRLF);
983 	my_printf("%s%s%s", ptr_filter_time, cCRLF, cCRLF);
984 	my_printf("%s%s", ptr_filter_scope, ptr_filter_groupname);
985 	my_flush();
986 }
987 
988 
989 #if defined(SIGWINCH) || defined(SIGTSTP)
990 void
refresh_filter_menu(void)991 refresh_filter_menu(
992 	void)
993 {
994 	print_filter_menu();
995 
996 	/*
997 	 * TODO:
998 	 * - refresh already entered and accepted information (follow control
999 	 *   flow in filter_menu below)
1000 	 * - refresh help line
1001 	 * - set cursor into current input field
1002 	 * - refresh already entered data or selected item in current input field
1003 	 *   (not everywhere possible yet -- must change getline.c for refreshing
1004 	 *    string input)
1005 	 */
1006 }
1007 #endif /* SIGWINCH || SIGTSTP */
1008 
1009 
1010 /*
1011  * a help function for filter_menu
1012  * formats a menu option in a multibyte-safe way
1013  *
1014  * this function in closely tight to the way how the filter menu is build
1015  */
1016 static void
fmt_filter_menu_prompt(char * dest,size_t dest_len,const char * fmt_str,int len,const char * text)1017 fmt_filter_menu_prompt(
1018 	char *dest,		/* where to store the resulting string */
1019 	size_t dest_len,	/* size of dest */
1020 	const char *fmt_str,	/* format string */
1021 	int len,		/* maximal len of the include string */
1022 	const char *text)	/* the include string */
1023 {
1024 	char *buf;
1025 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
1026 	wchar_t *wbuf, *wbuf2;
1027 
1028 	if ((wbuf = char2wchar_t(text)) != NULL) {
1029 		wbuf2 = wcspart(wbuf, len, TRUE);
1030 		if ((buf = wchar_t2char(wbuf2)) == NULL) {
1031 			/* conversion failed, truncate original string */
1032 			buf = my_malloc(len + 1);
1033 			snprintf(buf, len + 1, "%-*.*s", len, len, text);
1034 		}
1035 
1036 		free(wbuf);
1037 		free(wbuf2);
1038 	} else
1039 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
1040 	{
1041 		buf = my_malloc(len + 1);
1042 		snprintf(buf, len + 1, "%-*.*s", len, len, text);
1043 	}
1044 	snprintf(dest, dest_len, fmt_str, buf);
1045 	free(buf);
1046 }
1047 
1048 
1049 /*
1050  * Interactive filter menu so that the user can dynamically enter parameters.
1051  * Can be configured for kill or auto-selection screens.
1052  */
1053 t_bool
filter_menu(t_function type,struct t_group * group,struct t_article * art)1054 filter_menu(
1055 	t_function type,
1056 	struct t_group *group,
1057 	struct t_article *art)
1058 {
1059 	const char *ptr_filter_from;
1060 	const char *ptr_filter_msgid;
1061 	const char *ptr_filter_subj;
1062 	const char *ptr_filter_help_scope;
1063 	const char *ptr_filter_quit_edit_save;
1064 	char *ptr;
1065 	char **list;
1066 	char comment_line[LEN];
1067 	char buf[LEN];
1068 	char keyedit[MAXKEYLEN], keyquit[MAXKEYLEN], keysave[MAXKEYLEN];
1069 	char text_time[PATH_LEN];
1070 	char double_time[PATH_LEN];
1071 	char quat_time[PATH_LEN];
1072 	int i, len, clen, flen;
1073 	struct t_filter_rule rule;
1074 	t_bool proceed;
1075 	t_bool ret;
1076 	t_function func, default_func = FILTER_SAVE;
1077 
1078 	signal_context = cFilter;
1079 
1080 	rule.comment = (struct t_filter_comment *) 0;
1081 	rule.text[0] = '\0';
1082 	rule.scope[0] = '\0';
1083 	rule.counter = 0;
1084 	rule.lines_cmp = FILTER_LINES_NO;
1085 	rule.lines_num = 0;
1086 	rule.from_ok = FALSE;
1087 	rule.lines_ok = FALSE;
1088 	rule.msgid_ok = FALSE;
1089 	rule.fullref = FILTER_MSGID;
1090 	rule.subj_ok = FALSE;
1091 	rule.icase = FALSE;
1092 	rule.score = 0;
1093 	rule.expire_time = FALSE;
1094 	rule.check_string = FALSE;
1095 
1096 	comment_line[0] = '\0';
1097 
1098 	/*
1099 	 * setup correct text for user selected menu
1100 	 */
1101 	printascii(keyedit, func_to_key(FILTER_EDIT, filter_keys));
1102 	printascii(keyquit, func_to_key(GLOBAL_QUIT, filter_keys));
1103 	printascii(keysave, func_to_key(FILTER_SAVE, filter_keys));
1104 
1105 	if (type == GLOBAL_MENU_FILTER_KILL) {
1106 		ptr_filter_from = _(txt_kill_from);
1107 		ptr_filter_lines = _(txt_kill_lines);
1108 		ptr_filter_menu = _(txt_kill_menu);
1109 		ptr_filter_msgid = _(txt_kill_msgid);
1110 		ptr_filter_scope = _(txt_kill_scope);
1111 		ptr_filter_subj = _(txt_kill_subj);
1112 		ptr_filter_text = _(txt_kill_text);
1113 		ptr_filter_time = _(txt_kill_time);
1114 		ptr_filter_help_scope = _(txt_help_kill_scope);
1115 		ptr_filter_quit_edit_save = _(txt_quit_edit_save_kill);
1116 	} else {	/* type == GLOBAL_MENU_FILTER_SELECT */
1117 		ptr_filter_from = _(txt_select_from);
1118 		ptr_filter_lines = _(txt_select_lines);
1119 		ptr_filter_menu = _(txt_select_menu);
1120 		ptr_filter_msgid = _(txt_select_msgid);
1121 		ptr_filter_scope = _(txt_select_scope);
1122 		ptr_filter_subj = _(txt_select_subj);
1123 		ptr_filter_text = _(txt_select_text);
1124 		ptr_filter_time = _(txt_select_time);
1125 		ptr_filter_help_scope = _(txt_help_select_scope);
1126 		ptr_filter_quit_edit_save = _(txt_quit_edit_save_select);
1127 	}
1128 
1129 	ptr_filter_comment = _(txt_filter_comment);
1130 	ptr_filter_groupname = group->name;
1131 
1132 	clen = strwidth(_(txt_no));
1133 	clen = MAX(clen, strwidth(_(txt_yes)));
1134 	clen = MAX(clen, strwidth(_(txt_full)));
1135 	clen = MAX(clen, strwidth(_(txt_last)));
1136 	clen = MAX(clen, strwidth(_(txt_only)));
1137 
1138 	flen = strwidth(ptr_filter_subj) - 2;
1139 	flen = MAX(flen, strwidth(ptr_filter_from) - 2);
1140 	flen = MAX(flen, strwidth(ptr_filter_msgid) - 2);
1141 
1142 	len = cCOLS - flen - clen - 1 + 4;
1143 
1144 	snprintf(text_time, sizeof(text_time), _(txt_time_default_days), tinrc.filter_days);
1145 	fmt_filter_menu_prompt(text_subj, sizeof(text_subj), ptr_filter_subj, len, art->subject);
1146 	snprintf(text_score, sizeof(text_score), _(txt_filter_score), (type == GLOBAL_MENU_FILTER_KILL ? -tinrc.score_kill : tinrc.score_select));
1147 	fmt_filter_menu_prompt(text_from, sizeof(text_from), ptr_filter_from, len, art->from);
1148 	fmt_filter_menu_prompt(text_msgid, sizeof(text_msgid), ptr_filter_msgid, len - 4, MSGID(art));
1149 
1150 	print_filter_menu();
1151 
1152 	/*
1153 	 * None, one or multiple lines of comment.
1154 	 * Continue until an empty line is entered.
1155 	 * The empty line is ignored.
1156 	 */
1157 	show_menu_help(_(txt_help_filter_comment));
1158 	while ((proceed = prompt_menu_string(INDEX_TOP, ptr_filter_comment, comment_line)) && comment_line[0] != '\0') {
1159 		rule.comment = add_filter_comment(rule.comment, comment_line);
1160 		comment_line[0] = '\0';
1161 	}
1162 	if (!proceed) {
1163 		rule.comment = free_filter_comment(rule.comment);
1164 		return FALSE;
1165 	}
1166 
1167 	/*
1168 	 * Text which might be used to filter on subj, from or msgid
1169 	 */
1170 	show_menu_help(_(txt_help_filter_text));
1171 	if (!prompt_menu_string(INDEX_TOP + 2, ptr_filter_text, rule.text)) {
1172 		rule.comment = free_filter_comment(rule.comment);
1173 		return FALSE;
1174 	}
1175 
1176 	if (*rule.text) {
1177 		list = my_malloc(sizeof(char *) * 8);
1178 		list[0] = (char *) _(txt_subj_line_only_case);
1179 		list[1] = (char *) _(txt_subj_line_only);
1180 		list[2] = (char *) _(txt_from_line_only_case);
1181 		list[3] = (char *) _(txt_from_line_only);
1182 		list[4] = (char *) _(txt_msgid_refs_line);
1183 		list[5] = (char *) _(txt_msgid_line_last);
1184 		list[6] = (char *) _(txt_msgid_line_only);
1185 		list[7] = (char *) _(txt_refs_line_only);
1186 
1187 		i = get_choice(INDEX_TOP + 3, _(txt_help_filter_text_type), _(txt_filter_text_type), list, 8);
1188 		free(list);
1189 
1190 		if (i == -1) {
1191 			rule.comment = free_filter_comment(rule.comment);
1192 			return FALSE;
1193 		}
1194 
1195 		rule.counter = i;
1196 		switch (i) {
1197 			case FILTER_SUBJ_CASE_IGNORE:
1198 			case FILTER_FROM_CASE_IGNORE:
1199 				rule.icase = TRUE;
1200 				break;
1201 
1202 			case FILTER_SUBJ_CASE_SENSITIVE:
1203 			case FILTER_FROM_CASE_SENSITIVE:
1204 			case FILTER_MSGID:
1205 			case FILTER_MSGID_LAST:
1206 			case FILTER_MSGID_ONLY:
1207 			case FILTER_REFS_ONLY:
1208 				break;
1209 
1210 			default: /* should not happen */
1211 				/* CONSTANTCONDITION */
1212 				assert(0 != 0);
1213 				break;
1214 		}
1215 	}
1216 
1217 	if (!*rule.text) {
1218 		rule.check_string = TRUE;
1219 		/*
1220 		 * Subject:
1221 		 */
1222 		list = my_malloc(sizeof(char *) * 2);
1223 		list[0] = (char *) _(txt_yes);
1224 		list[1] = (char *) _(txt_no);
1225 		i = get_choice(INDEX_TOP + 5, _(txt_help_filter_subj), text_subj, list, 2);
1226 		free(list);
1227 
1228 		if (i == -1) {
1229 			rule.comment = free_filter_comment(rule.comment);
1230 			return FALSE;
1231 		} else
1232 			rule.subj_ok = (i == 0);
1233 
1234 		/*
1235 		 * From:
1236 		 */
1237 		list = my_malloc(sizeof(char *) * 2);
1238 		if (rule.subj_ok) {
1239 			list[0] = (char *) _(txt_no);
1240 			list[1] = (char *) _(txt_yes);
1241 		} else {
1242 			list[0] = (char *) _(txt_yes);
1243 			list[1] = (char *) _(txt_no);
1244 		}
1245 		i = get_choice(INDEX_TOP + 6, _(txt_help_filter_from), text_from, list, 2);
1246 		free(list);
1247 
1248 		if (i == -1) {
1249 			rule.comment = free_filter_comment(rule.comment);
1250 			return FALSE;
1251 		} else
1252 			rule.from_ok = rule.subj_ok ? (i != 0) : (i == 0);
1253 
1254 		/*
1255 		 * Message-ID:
1256 		 */
1257 		list = my_malloc(sizeof(char *) * 4);
1258 		if (rule.subj_ok || rule.from_ok) {
1259 			list[0] = (char *) _(txt_no);
1260 			list[1] = (char *) _(txt_full);
1261 			list[2] = (char *) _(txt_last);
1262 			list[3] = (char *) _(txt_only);
1263 		} else {
1264 			list[0] = (char *) _(txt_full);
1265 			list[1] = (char *) _(txt_last);
1266 			list[2] = (char *) _(txt_only);
1267 			list[3] = (char *) _(txt_no);
1268 		}
1269 		i = get_choice(INDEX_TOP + 7, _(txt_help_filter_msgid), text_msgid, list, 4);
1270 		free(list);
1271 
1272 		if (i == -1) {
1273 			rule.comment = free_filter_comment(rule.comment);
1274 			return FALSE;
1275 		} else {
1276 			switch ((rule.subj_ok || rule.from_ok) ? i : i + 1) {
1277 				case 0:
1278 				case 4:
1279 					rule.msgid_ok = FALSE;
1280 					rule.fullref = FILTER_MSGID;
1281 					break;
1282 
1283 				case 1:
1284 					rule.msgid_ok = TRUE;
1285 					rule.fullref = FILTER_MSGID;
1286 					break;
1287 
1288 				case 2:
1289 					rule.msgid_ok = TRUE;
1290 					rule.fullref = FILTER_MSGID_LAST;
1291 					break;
1292 
1293 				case 3:
1294 					rule.msgid_ok = TRUE;
1295 					rule.fullref = FILTER_MSGID_ONLY;
1296 					break;
1297 
1298 				default: /* should not happen */
1299 					/* CONSTANTCONDITION */
1300 					assert(0 != 0);
1301 					break;
1302 			}
1303 		}
1304 
1305 	}
1306 
1307 	/*
1308 	 * Lines:
1309 	 */
1310 	show_menu_help(_(txt_help_filter_lines));
1311 
1312 	buf[0] = '\0';
1313 
1314 	if (!prompt_menu_string(INDEX_TOP + 9, ptr_filter_lines, buf)) {
1315 		rule.comment = free_filter_comment(rule.comment);
1316 		return FALSE;
1317 	}
1318 
1319 	/*
1320 	 * Get the < > sign if any for the lines rule
1321 	 */
1322 	ptr = buf;
1323 	while (*ptr == ' ')
1324 		ptr++;
1325 
1326 	if (*ptr == '>') {
1327 		rule.lines_cmp = FILTER_LINES_GT;
1328 		ptr++;
1329 	} else if (*ptr == '<') {
1330 		rule.lines_cmp = FILTER_LINES_LT;
1331 		ptr++;
1332 	} else if (*ptr == '=') {
1333 		rule.lines_cmp = FILTER_LINES_EQ;
1334 		ptr++;
1335 	}
1336 
1337 	if (*ptr)
1338 		rule.lines_num = abs(atoi(ptr));
1339 
1340 	if (rule.lines_num && rule.lines_cmp == FILTER_LINES_NO)
1341 		rule.lines_cmp = FILTER_LINES_EQ;
1342 
1343 	if (rule.lines_cmp != FILTER_LINES_NO && rule.lines_num)
1344 		rule.lines_ok = TRUE;
1345 
1346 	/*
1347 	 * Scoring value
1348 	 */
1349 	snprintf(buf, sizeof(buf), _(txt_filter_score_help), SCORE_MAX);
1350 	show_menu_help(buf);
1351 
1352 	buf[0] = '\0';
1353 	if (!prompt_menu_string(INDEX_TOP + 10, text_score, buf)) {
1354 		rule.comment = free_filter_comment(rule.comment);
1355 		return FALSE;
1356 	}
1357 
1358 	/* check if a score has been entered */
1359 	if (buf[0] != '\0')
1360 		/* use entered score */
1361 		rule.score = atoi(buf);
1362 	else {
1363 		/* use default score */
1364 		if (type == GLOBAL_MENU_FILTER_KILL)
1365 			rule.score = tinrc.score_kill;
1366 		else /* type == GLOBAL_MENU_FILTER_SELECT */
1367 			rule.score = tinrc.score_select;
1368 	}
1369 
1370 	if (!rule.score) { /* ignore 0 scores */
1371 		rule.comment = free_filter_comment(rule.comment);
1372 		return FALSE;
1373 	}
1374 
1375 	/*
1376 	 * assure we are in range
1377 	 */
1378 	if (rule.score < 0)
1379 		rule.score = abs(rule.score);
1380 	if (rule.score > SCORE_MAX)
1381 		rule.score = SCORE_MAX;
1382 
1383 	/* get the right sign for the score */
1384 	if (type == GLOBAL_MENU_FILTER_KILL)
1385 		rule.score = -rule.score;
1386 
1387 	/*
1388 	 * Expire time
1389 	 */
1390 	snprintf(double_time, sizeof(double_time), "2x %s", text_time);
1391 	snprintf(quat_time, sizeof(quat_time), "4x %s", text_time);
1392 	list = my_malloc(sizeof(char *) * 4);
1393 	list[0] = (char *) _(txt_unlimited_time);
1394 	list[1] = text_time;
1395 	list[2] = double_time;
1396 	list[3] = quat_time;
1397 	i = get_choice(INDEX_TOP + 11, _(txt_help_filter_time), ptr_filter_time, list, 4);
1398 	free(list);
1399 
1400 	if (i == -1) {
1401 		rule.comment = free_filter_comment(rule.comment);
1402 		return FALSE;
1403 	}
1404 
1405 	rule.expire_time = i;
1406 
1407 	/*
1408 	 * Scope
1409 	 */
1410 	if (*rule.text || rule.subj_ok || rule.from_ok || rule.msgid_ok || rule.lines_ok) {
1411 		int j = 0;
1412 
1413 		list = my_malloc(sizeof(char *) * 2); /* at least 2 scopes */
1414 		list[j++] = my_strdup(group->name);
1415 		list[j] = my_strdup(list[j - 1]);
1416 		while ((ptr = strrchr(list[j], '.')) != NULL) {
1417 			*(++ptr) = '*';
1418 			*(++ptr) = '\0';
1419 			j++;
1420 			list = my_realloc(list, sizeof(char *) * (j + 1)); /* one element more */
1421 			list[j] = my_strdup(list[j - 1]);
1422 			list[j][strlen(list[j]) - 2] = '\0';
1423 		}
1424 		free(list[j]); /* this copy isn't needed anymore */
1425 		list[j] = (char *) _(txt_all_groups);
1426 
1427 		if ((i = get_choice(INDEX_TOP + 13, ptr_filter_help_scope, ptr_filter_scope, list, j + 1)) > 0)
1428 			my_strncpy(rule.scope, i == j ? "*" : list[i], sizeof(rule.scope) - 1);
1429 
1430 		for (j--; j >= 0; j--)
1431 			free(list[j]);
1432 		free(list);
1433 
1434 		if (i == -1) {
1435 			rule.comment = free_filter_comment(rule.comment);
1436 			return FALSE;
1437 		}
1438 	} else {
1439 		rule.comment = free_filter_comment(rule.comment);
1440 		return FALSE;
1441 	}
1442 
1443 	forever {
1444 		func = prompt_slk_response(default_func, filter_keys,
1445 				ptr_filter_quit_edit_save, keyquit, keyedit, keysave);
1446 		switch (func) {
1447 
1448 		case FILTER_EDIT:
1449 			add_filter_rule(group, art, &rule, FALSE); /* save the rule */
1450 			rule.comment = free_filter_comment(rule.comment);
1451 			if (!invoke_editor(filter_file, filter_file_offset, NULL))
1452 				return FALSE;
1453 			unfilter_articles(group);
1454 			(void) read_filter_file(filter_file);
1455 			return TRUE;
1456 			/* keep lint quiet: */
1457 			/* FALLTHROUGH */
1458 
1459 		case GLOBAL_QUIT:
1460 		case GLOBAL_ABORT:
1461 			rule.comment = free_filter_comment(rule.comment);
1462 			return FALSE;
1463 			/* keep lint quiet: */
1464 			/* FALLTHROUGH */
1465 
1466 		case FILTER_SAVE:
1467 			/*
1468 			 * Add the filter rule and save it to the filter file
1469 			 */
1470 			ret = add_filter_rule(group, art, &rule, FALSE);
1471 			rule.comment = free_filter_comment(rule.comment);
1472 			return ret;
1473 			/* keep lint quiet: */
1474 			/* FALLTHROUGH */
1475 
1476 		default:
1477 			break;
1478 		}
1479 	}
1480 	/* NOTREACHED */
1481 	return FALSE;
1482 }
1483 
1484 
1485 /*
1486  * Quick command to add an auto-select / kill filter to specified groups filter
1487  */
1488 t_bool
quick_filter(t_function type,struct t_group * group,struct t_article * art)1489 quick_filter(
1490 	t_function type,
1491 	struct t_group *group,
1492 	struct t_article *art)
1493 {
1494 	char *scope;
1495 	char txt[LEN];
1496 	int header, expire, icase;
1497 	struct t_filter_rule rule;
1498 	t_bool ret;
1499 
1500 	if (type == GLOBAL_QUICK_FILTER_KILL) {
1501 		header = group->attribute->quick_kill_header;
1502 		expire = group->attribute->quick_kill_expire;
1503 		/* ON=case sensitive, OFF=ignore case -> invert */
1504 		icase = bool_not(group->attribute->quick_kill_case);
1505 		scope = group->attribute->quick_kill_scope;
1506 	} else {	/* type == GLOBAL_QUICK_FILTER_SELECT */
1507 		header = group->attribute->quick_select_header;
1508 		expire = group->attribute->quick_select_expire;
1509 		/* ON=case sensitive, OFF=ignore case -> invert */
1510 		icase = bool_not(group->attribute->quick_select_case);
1511 		scope = group->attribute->quick_select_scope;
1512 	}
1513 
1514 #ifdef DEBUG
1515 	if (debug & DEBUG_FILTER)
1516 		error_message(2, "%s header=[%d] scope=[%s] expire=[%s] case=[%d]", (type == GLOBAL_QUICK_FILTER_KILL) ? "KILL" : "SELECT", header, BlankIfNull(scope), txt_onoff[expire != FALSE ? 1 : 0], icase);
1517 #endif /* DEBUG */
1518 
1519 	/*
1520 	 * Setup rules
1521 	 */
1522 	if (strlen(BlankIfNull(scope)) > (sizeof(rule.scope) - 1))
1523 		return FALSE;
1524 	my_strncpy(rule.scope, BlankIfNull(scope), sizeof(rule.scope) - 1);
1525 	rule.counter = 0;
1526 	rule.lines_cmp = FILTER_LINES_NO;
1527 	rule.lines_num = 0;
1528 	rule.lines_ok = (header == FILTER_LINES);
1529 	rule.msgid_ok = (header == FILTER_MSGID) || (header == FILTER_MSGID_LAST);
1530 	rule.fullref = header; /* value is directly used to select correct filter type */
1531 	rule.from_ok = (header == FILTER_FROM_CASE_SENSITIVE || header == FILTER_FROM_CASE_IGNORE);
1532 	rule.subj_ok = (header == FILTER_SUBJ_CASE_SENSITIVE || header == FILTER_SUBJ_CASE_IGNORE);
1533 
1534 	/* create an auto-comment. */
1535 	if (type == GLOBAL_QUICK_FILTER_KILL)
1536 		snprintf(txt, sizeof(txt), "%s%s%c%s%s%s", _(txt_filter_rule_created), "'", ']', "' (", _(txt_help_article_quick_kill), ").");
1537 	else
1538 		snprintf(txt, sizeof(txt), "%s%s%c%s%s%s", _(txt_filter_rule_created), "'", '[', "' (", _(txt_help_article_quick_select), ").");
1539 	rule.comment = add_filter_comment(NULL, txt);
1540 
1541 	rule.text[0] = '\0';
1542 	rule.icase = icase;
1543 	rule.expire_time = expire;
1544 	rule.check_string = TRUE;
1545 	rule.score = (type == GLOBAL_QUICK_FILTER_KILL) ? tinrc.score_kill : tinrc.score_select;
1546 
1547 	ret = add_filter_rule(group, art, &rule, TRUE);
1548 	rule.comment = free_filter_comment(rule.comment);
1549 	return ret;
1550 }
1551 
1552 
1553 /*
1554  * Quick command to add an auto-select filter to the article that user
1555  * has just posted. Selects on Subject: line with limited expire time.
1556  * Don't process if GROUP_TYPE_MAIL || GROUP_TYPE_SAVE
1557  */
1558 t_bool
quick_filter_select_posted_art(struct t_group * group,const char * subj,const char * a_message_id)1559 quick_filter_select_posted_art(
1560 	struct t_group *group,
1561 	const char *subj,
1562 	const char *a_message_id)	/* return value is always ignored */
1563 {
1564 	t_bool filtered = FALSE;
1565 	char txt[LEN];
1566 
1567 	if (group->type == GROUP_TYPE_NEWS) {
1568 		struct t_article art;
1569 		struct t_filter_rule rule;
1570 
1571 #ifdef __cplusplus /* keep C++ quiet */
1572 		rule.scope[0] = '\0';
1573 #endif /* __cplusplus */
1574 
1575 		if (strlen(group->name) > (sizeof(rule.scope) - 1)) /* groupname to long? */
1576 			return FALSE;
1577 
1578 		/*
1579 		 * Setup rules
1580 		 */
1581 		rule.counter = 0;
1582 		rule.lines_cmp = FILTER_LINES_NO;
1583 		rule.lines_num = 0;
1584 		rule.from_ok = FALSE;
1585 		rule.lines_ok = FALSE;
1586 		rule.msgid_ok = FALSE;
1587 		rule.fullref = FILTER_MSGID;
1588 		rule.subj_ok = TRUE;
1589 		rule.text[0] = '\0';
1590 		rule.icase = FALSE;
1591 		rule.expire_time = TRUE;
1592 		rule.check_string = TRUE;
1593 		rule.score = tinrc.score_select;
1594 
1595 		strcpy(rule.scope, group->name);
1596 
1597 		/* create an auto-comment. */
1598 		snprintf(txt, sizeof(txt), "%s%s", _(txt_filter_rule_created), "add_posted_to_filter=ON.");
1599 		rule.comment = add_filter_comment(NULL, txt);
1600 
1601 		/*
1602 		 * Setup dummy article with posted articles subject
1603 		 * xor Message-ID
1604 		 */
1605 		set_article(&art);
1606 		if (*a_message_id) {
1607 			/* initialize art->refptr */
1608 			struct {
1609 				struct t_msgid *next;
1610 				struct t_msgid *parent;
1611 				struct t_msgid *sibling;
1612 				struct t_msgid *child;
1613 				int article;
1614 				char txt[HEADER_LEN];
1615 			} refptr_dummyart;
1616 
1617 			rule.subj_ok = FALSE;
1618 			rule.msgid_ok = TRUE;
1619 			refptr_dummyart.next = (struct t_msgid *) 0;
1620 			refptr_dummyart.parent = (struct t_msgid *) 0;
1621 			refptr_dummyart.sibling = (struct t_msgid *) 0;
1622 			refptr_dummyart.child = (struct t_msgid *) 0;
1623 			refptr_dummyart.article = ART_NORMAL;
1624 			my_strncpy(refptr_dummyart.txt, a_message_id, HEADER_LEN);
1625 			/* Hack */
1626 			art.refptr = (struct t_msgid *) &refptr_dummyart;
1627 
1628 			filtered = add_filter_rule(group, &art, &rule, FALSE);
1629 		} else {
1630 			art.subject = my_strdup(subj);
1631 			filtered = add_filter_rule(group, &art, &rule, FALSE);
1632 			FreeIfNeeded(art.subject);
1633 		}
1634 		rule.comment = free_filter_comment(rule.comment);
1635 	}
1636 	return filtered;
1637 }
1638 
1639 
1640 /*
1641  * API to add filter rule to the local or global filter array
1642  */
1643 static t_bool
add_filter_rule(struct t_group * group,struct t_article * art,struct t_filter_rule * rule,t_bool quick_filter_rule)1644 add_filter_rule(
1645 	struct t_group *group,
1646 	struct t_article *art,
1647 	struct t_filter_rule *rule,
1648 	t_bool quick_filter_rule)
1649 {
1650 	char acbuf[PATH_LEN];
1651 	char sbuf[(sizeof(acbuf) / 2)]; /* half as big as acbuf so quote_wild(sbuf) fits into acbuf */
1652 	int i = glob_filter.num;
1653 	t_bool filtered = FALSE;
1654 	time_t current_time;
1655 	struct t_filter *ptr;
1656 
1657 	if (glob_filter.num >= glob_filter.max)
1658 		expand_filter_array(&glob_filter);
1659 
1660 	ptr = glob_filter.filter;
1661 
1662 	ptr[i].inscope = TRUE;
1663 	ptr[i].icase = FALSE;
1664 	ptr[i].fullref = FILTER_MSGID;
1665 	ptr[i].comment = (struct t_filter_comment *) 0;
1666 	ptr[i].scope = NULL;
1667 	ptr[i].subj = NULL;
1668 	ptr[i].from = NULL;
1669 	ptr[i].msgid = NULL;
1670 	ptr[i].lines_cmp = rule->lines_cmp;
1671 	ptr[i].lines_num = rule->lines_num;
1672 	ptr[i].gnksa_cmp = FILTER_LINES_NO;
1673 	ptr[i].gnksa_num = 0;
1674 	ptr[i].score = rule->score;
1675 	ptr[i].xref = NULL;
1676 	ptr[i].path = NULL;
1677 
1678 	if (rule->comment != NULL)
1679 		ptr[i].comment = copy_filter_comment(rule->comment, ptr[i].comment);
1680 
1681 	if (rule->scope[0] == '\0') /* replace empty scope with current group name */
1682 		ptr[i].scope = my_strdup(group->name);
1683 	else {
1684 		if ((rule->scope[0] != '*') && (rule->scope[1] != '\0')) /* copy non-global scope */
1685 			ptr[i].scope = my_strdup(rule->scope);
1686 	}
1687 
1688 	(void) time(&current_time);
1689 	switch (rule->expire_time) {
1690 		case 1:
1691 			ptr[i].time = current_time + (time_t) (tinrc.filter_days * DAY);
1692 			break;
1693 
1694 		case 2:
1695 			ptr[i].time = current_time + (time_t) (tinrc.filter_days * DAY * 2);
1696 			break;
1697 
1698 		case 3:
1699 			ptr[i].time = current_time + (time_t) (tinrc.filter_days * DAY * 4);
1700 			break;
1701 
1702 		default:
1703 			ptr[i].time = (time_t) 0;
1704 			break;
1705 	}
1706 
1707 	ptr[i].icase = rule->icase;
1708 	if (*rule->text) {
1709 		snprintf(acbuf, sizeof(acbuf), REGEX_FMT, quote_wild_whitespace(rule->text));
1710 
1711 		switch (rule->counter) {
1712 			case FILTER_SUBJ_CASE_IGNORE:
1713 			case FILTER_SUBJ_CASE_SENSITIVE:
1714 				ptr[i].subj = my_strdup(acbuf);
1715 				break;
1716 
1717 			case FILTER_FROM_CASE_IGNORE:
1718 			case FILTER_FROM_CASE_SENSITIVE:
1719 				ptr[i].from = my_strdup(acbuf);
1720 				break;
1721 
1722 			case FILTER_MSGID:
1723 			case FILTER_MSGID_LAST:
1724 			case FILTER_MSGID_ONLY:
1725 			case FILTER_REFS_ONLY:
1726 				ptr[i].msgid = my_strdup(acbuf);
1727 				ptr[i].fullref = rule->counter;
1728 				break;
1729 
1730 			default: /* should not happen */
1731 				/* CONSTANTCONDITION */
1732 				assert(0 != 0);
1733 				break;
1734 		}
1735 		filtered = TRUE;
1736 		glob_filter.num++;
1737 	} else {
1738 		/*
1739 		 * STRCPY() truncates subject/from/message-id so it fits
1740 		 * into acbuf even after quote_wild()
1741 		 */
1742 		if (rule->subj_ok) {
1743 			STRCPY(sbuf, art->subject);
1744 			snprintf(acbuf, sizeof(acbuf), REGEX_FMT, (rule->check_string ? quote_wild(sbuf) : sbuf));
1745 			ptr[i].subj = my_strdup(acbuf);
1746 		}
1747 		if (rule->from_ok) {
1748 			STRCPY(sbuf, art->from);
1749 			snprintf(acbuf, sizeof(acbuf), REGEX_FMT, quote_wild(sbuf));
1750 			ptr[i].from = my_strdup(acbuf);
1751 		}
1752 		/*
1753 		 * message-ids should be quoted
1754 		 */
1755 		if (rule->msgid_ok) {
1756 			/*
1757 			 * If threading by references is set, and a quick kill is applied
1758 			 * (in group level), it is applied with the data of the root
1759 			 * article of the thread built by tin.
1760 			 * In case of threading by references, if tin's root is not the
1761 			 * *real* root of thread (which is the first entry in references
1762 			 * field) any applying of filtering for MSGID (or MSGID_LAST)
1763 			 * doesn't work, because the filter is applied with the data of
1764 			 * tin's root article which doesn't cover the other articles in
1765 			 * the thread.
1766 			 * So the thread remains open (in group level). To overcome this,
1767 			 * the first msgid from references field is taken in this case.
1768 			 */
1769 			if (quick_filter_rule && group->attribute->thread_articles == THREAD_REFS &&
1770 				(group->attribute->quick_kill_header == FILTER_MSGID ||
1771 				 group->attribute->quick_kill_header == FILTER_REFS_ONLY) &&
1772 				 art->refptr->parent != NULL)
1773 			{
1774 				/* not real parent, take first references entry as MID */
1775 				struct t_msgid *xptr;
1776 
1777 				for (xptr = art->refptr->parent; xptr->parent != NULL; xptr = xptr->parent)
1778 					;
1779 				STRCPY(sbuf, xptr->txt);
1780 			} else {
1781 				STRCPY(sbuf, MSGID(art));
1782 			}
1783 			snprintf(acbuf, sizeof(acbuf), REGEX_FMT, quote_wild(sbuf));
1784 			ptr[i].msgid = my_strdup(acbuf);
1785 			ptr[i].fullref = rule->fullref;
1786 		}
1787 		if (rule->subj_ok || rule->from_ok || rule->msgid_ok || rule->lines_ok) {
1788 			filtered = TRUE;
1789 			glob_filter.num++;
1790 		}
1791 	}
1792 
1793 	if (filtered) {
1794 #ifdef DEBUG
1795 		if (debug & DEBUG_FILTER)
1796 			wait_message(2, "inscope=[%s] scope=[%s] case=[%d] subj=[%s] from=[%s] msgid=[%s] fullref=[%u] line=[%d %d] time=[%lu]", bool_unparse(ptr[i].inscope), BlankIfNull(rule->scope), ptr[i].icase, BlankIfNull(ptr[i].subj), BlankIfNull(ptr[i].from), BlankIfNull(ptr[i].msgid), ptr[i].fullref, ptr[i].lines_cmp, ptr[i].lines_num, (unsigned long int) ptr[i].time);
1797 #endif /* DEBUG */
1798 		write_filter_file(filter_file);
1799 	}
1800 	return filtered;
1801 }
1802 
1803 
1804 /*
1805  * We assume that any articles which are tagged as killed are also
1806  * tagged as being read BECAUSE they were killed. We retag them as
1807  * being unread if they were unread before killing (ART_KILLED_UNREAD).
1808  * Selected articles will be un"select"ed.
1809  */
1810 void
unfilter_articles(struct t_group * group)1811 unfilter_articles(
1812 	struct t_group *group)
1813 {
1814 	int i;
1815 
1816 	for_each_art(i) {
1817 		arts[i].score = 0;
1818 		if (IS_KILLED(i)) {
1819 			if (IS_KILLED_UNREAD(i))
1820 				art_mark(group, &arts[i], ART_UNREAD);
1821 			arts[i].killed = ART_NOTKILLED;
1822 		}
1823 		if (IS_SELECTED(i))
1824 			arts[i].selected = FALSE;
1825 	}
1826 }
1827 
1828 
1829 /*
1830  * Filter any articles in specified group.
1831  * Apply global filter rules followed by group filter rules.
1832  * In global rules check if scope field set to determine if
1833  * filter applies to current group.
1834  */
1835 t_bool
filter_articles(struct t_group * group)1836 filter_articles(
1837 	struct t_group *group)
1838 {
1839 	char buf[LEN];
1840 	int num, inscope;
1841 	int i, j;
1842 	struct t_filter *ptr;
1843 	struct regex_cache *regex_cache_subj = NULL;
1844 	struct regex_cache *regex_cache_from = NULL;
1845 	struct regex_cache *regex_cache_msgid = NULL;
1846 	struct regex_cache *regex_cache_xref = NULL;
1847 	struct regex_cache *regex_cache_path = NULL;
1848 	t_bool filtered = FALSE;
1849 	t_bool error = FALSE;
1850 
1851 	/*
1852 	 * check if there are any global filter rules
1853 	 */
1854 	if (group->glob_filter->num == 0)
1855 		return filtered;
1856 
1857 	/*
1858 	 * Apply global filter rules first if there are any entries
1859 	 */
1860 	/*
1861 	 * Check if any scope rules are active for this group
1862 	 * ie. group=comp.os.linux.help  scope=comp.os.linux.*
1863 	 */
1864 	inscope = set_filter_scope(group);
1865 	if (!cmd_line && !batch_mode)
1866 		wait_message(0, _(txt_filter_global_rules), inscope, group->glob_filter->num);
1867 	num = group->glob_filter->num;
1868 	ptr = group->glob_filter->filter;
1869 
1870 	/*
1871 	 * set up cache tables for all types of filter rules
1872 	 * (only for regexp matching)
1873 	 */
1874 	if (tinrc.wildcard) {
1875 		size_t msiz;
1876 
1877 		msiz = sizeof(struct regex_cache) * num;
1878 		regex_cache_subj = my_malloc(msiz);
1879 		regex_cache_from = my_malloc(msiz);
1880 		regex_cache_msgid = my_malloc(msiz);
1881 		regex_cache_xref = my_malloc(msiz);
1882 		regex_cache_path = my_malloc(msiz);
1883 		for (j = 0; j < num; j++) {
1884 			regex_cache_subj[j].re = NULL;
1885 			regex_cache_subj[j].extra = NULL;
1886 			regex_cache_from[j].re = NULL;
1887 			regex_cache_from[j].extra = NULL;
1888 			regex_cache_msgid[j].re = NULL;
1889 			regex_cache_msgid[j].extra = NULL;
1890 			regex_cache_xref[j].re = NULL;
1891 			regex_cache_xref[j].extra = NULL;
1892 			regex_cache_path[j].re = NULL;
1893 			regex_cache_path[j].extra = NULL;
1894 		}
1895 	}
1896 
1897 	/*
1898 	 * loop through all arts applying global & local filtering rules
1899 	 */
1900 	for (i = 0; (i < top_art) && !error; i++) {
1901 #ifdef HAVE_SELECT
1902 		if (wait_for_input())   /* allow abort */
1903 			break; /* to free the mem */
1904 #endif /* HAVE_SELECT */
1905 
1906 		arts[i].score = 0;
1907 
1908 		if (tinrc.kill_level == KILL_UNREAD && IS_READ(i)) /* skip only when the article is read */
1909 			continue;
1910 
1911 		for (j = 0; j < num && !error; j++) {
1912 			if (ptr[j].inscope) {
1913 				/*
1914 				 * Filter on Subject: line
1915 				 */
1916 				if (ptr[j].subj != NULL) {
1917 					switch (test_regex(arts[i].subject, ptr[j].subj, ptr[j].icase, &regex_cache_subj[j])) {
1918 						case 1:
1919 							SET_FILTER(group, i, j);
1920 							break;
1921 
1922 						case -1:
1923 							error = TRUE;
1924 							break;
1925 
1926 						default:
1927 							break;
1928 					}
1929 				}
1930 
1931 				/*
1932 				 * Filter on From: line
1933 				 */
1934 				if (ptr[j].from != NULL) {
1935 					if (arts[i].name != NULL)
1936 						snprintf(buf, sizeof(buf), "%s (%s)", arts[i].from, arts[i].name);
1937 					else
1938 						STRCPY(buf, arts[i].from);
1939 
1940 					switch (test_regex(buf, ptr[j].from, ptr[j].icase, &regex_cache_from[j])) {
1941 						case 1:
1942 							SET_FILTER(group, i, j);
1943 							break;
1944 
1945 						case -1:
1946 							error = TRUE;
1947 							break;
1948 
1949 						default:
1950 							break;
1951 					}
1952 				}
1953 
1954 				/*
1955 				 * Filter on Message-ID: line
1956 				 * Apply to Message-ID: & References: lines or
1957 				 * Message-ID: & last entry from References: line
1958 				 * Case is important here
1959 				 */
1960 				if (ptr[j].msgid != NULL) {
1961 					char *refs = NULL;
1962 					const char *myrefs = NULL;
1963 					const char *mymsgid = NULL;
1964 					int x;
1965 					struct t_article *art = &arts[i];
1966 					/*
1967 					 * TODO: nice idea del'd; better apply one rule on all
1968 					 *       fitting articles, so we can switch to an appropriate
1969 					 *       algorithm for each kind of rule, including the
1970 					 *       deleted one.
1971 					 */
1972 
1973 					/* myrefs does not need to be freed */
1974 
1975 					/* use full references header or just the last entry? */
1976 					switch (ptr[j].fullref) {
1977 						case FILTER_MSGID:
1978 							myrefs = REFS(art, refs);
1979 							mymsgid = MSGID(art);
1980 							break;
1981 
1982 						case FILTER_MSGID_LAST:
1983 							myrefs = art->refptr ? (art->refptr->parent ? art->refptr->parent->txt : "") : "";
1984 							mymsgid = MSGID(art);
1985 							break;
1986 
1987 						case FILTER_MSGID_ONLY:
1988 							myrefs = "";
1989 							mymsgid = MSGID(art);
1990 							break;
1991 
1992 						case FILTER_REFS_ONLY:
1993 							myrefs = REFS(art, refs);
1994 							mymsgid = "";
1995 							break;
1996 
1997 						default: /* should not happen */
1998 							/* CONSTANTCONDITION */
1999 							assert(0 != 0);
2000 							break;
2001 					}
2002 
2003 					if ((x = test_regex(myrefs, ptr[j].msgid, FALSE, &regex_cache_msgid[j])) == 0) /* no match */
2004 						x = test_regex(mymsgid, ptr[j].msgid, FALSE, &regex_cache_msgid[j]);
2005 
2006 					switch (x) {
2007 						case 1:
2008 							SET_FILTER(group, i, j);
2009 #ifdef DEBUG
2010 							if (debug & DEBUG_FILTER)
2011 								debug_print_file("FILTER", "Group[%s] Rule[%d][%s] ArtMSGID[%s] Artnum[%d]", group->name, j, ptr[j].msgid, mymsgid, i);
2012 #endif /* DEBUG */
2013 							break;
2014 
2015 						case -1:
2016 							error = TRUE;
2017 							break;
2018 
2019 						default:
2020 							break;
2021 					}
2022 					FreeIfNeeded(refs);
2023 				}
2024 				/*
2025 				 * Filter on Lines: line
2026 				 */
2027 				if ((ptr[j].lines_cmp != FILTER_LINES_NO) && (arts[i].line_count >= 0)) {
2028 					switch (ptr[j].lines_cmp) {
2029 						case FILTER_LINES_EQ:
2030 							if (arts[i].line_count == ptr[j].lines_num) {
2031 								SET_FILTER(group, i, j);
2032 							}
2033 							break;
2034 
2035 						case FILTER_LINES_LT:
2036 							if (arts[i].line_count < ptr[j].lines_num) {
2037 								SET_FILTER(group, i, j);
2038 							}
2039 							break;
2040 
2041 						case FILTER_LINES_GT:
2042 							if (arts[i].line_count > ptr[j].lines_num) {
2043 								SET_FILTER(group, i, j);
2044 							}
2045 							break;
2046 
2047 						default:
2048 							break;
2049 					}
2050 				}
2051 
2052 				/*
2053 				 * Filter on GNKSA code
2054 				 */
2055 				if ((ptr[j].gnksa_cmp != FILTER_LINES_NO) && (arts[i].gnksa_code >= 0)) {
2056 					switch (ptr[j].gnksa_cmp) {
2057 						case FILTER_LINES_EQ:
2058 							if (arts[i].gnksa_code == ptr[j].gnksa_num) {
2059 								SET_FILTER(group, i, j);
2060 							}
2061 							break;
2062 
2063 						case FILTER_LINES_LT:
2064 							if (arts[i].gnksa_code < ptr[j].gnksa_num) {
2065 								SET_FILTER(group, i, j);
2066 							}
2067 							break;
2068 
2069 						case FILTER_LINES_GT:
2070 							if (arts[i].gnksa_code > ptr[j].gnksa_num) {
2071 								SET_FILTER(group, i, j);
2072 							}
2073 							break;
2074 
2075 						default:
2076 							break;
2077 					}
2078 				}
2079 
2080 				/*
2081 				 * Filter on Xref: lines
2082 				 *
2083 				 * "news.foo.bar foo.bar:666 bar.bar:999"
2084 				 * is turned into the Newsgroups like string
2085 				 * foo.bar,bar.bar
2086 				 */
2087 				if (arts[i].xref && *arts[i].xref) {
2088 					if (ptr[j].score && ptr[j].xref != NULL) {
2089 						char *s, *e, *k;
2090 						t_bool skip = FALSE;
2091 
2092 						s = arts[i].xref;
2093 						if (strchr(s, ' ') || strchr(s, '\t')) {
2094 							while (*s && !isspace((int) *s))	/* skip server name */
2095 								s++;
2096 							while (*s && isspace((int) *s))
2097 								s++;
2098 						}
2099 #ifdef DEBUG
2100 						else { /* server name missing in overview, i.e. colobus 2.1 */
2101 							if (debug & DEBUG_FILTER) { /* TODO: lang.c, _()? */
2102 								debug_print_file("FILTER", "Malformed overview entry: servername missing.");
2103 								debug_print_file("FILTER", "\t Xref: %s", arts[i].xref);
2104 							}
2105 						}
2106 #endif /* DEBUG */
2107 						if (strlen(s)) {
2108 							/* reformat */
2109 							k = e = my_malloc(strlen(s) + 1);
2110 							while (*s) {
2111 								if (*s == ':') {
2112 									*e++ = ',';
2113 									skip = TRUE;
2114 								}
2115 								if (*s != ':' && !isspace((int) *s) && !skip)
2116 									*e++ = *s;
2117 								if (isspace((int) *s))
2118 									skip = FALSE;
2119 								s++;
2120 							}
2121 							*--e = '\0';
2122 						} else {
2123 #ifdef DEBUG
2124 							if (debug & DEBUG_FILTER) /* TODO: lang.c, _()? */
2125 								debug_print_file("FILTER", "Skipping Xref filter");
2126 #endif /* DEBUG */
2127 							error = TRUE;
2128 							break;
2129 						}
2130 
2131 						switch (test_regex(k, ptr[j].xref, ptr[j].icase, &regex_cache_xref[j])) {
2132 							case 1:
2133 								SET_FILTER(group, i, j);
2134 								break;
2135 
2136 							case -1:
2137 								error = TRUE;
2138 								break;
2139 
2140 							default:
2141 								break;
2142 						}
2143 						free(k);
2144 					}
2145 				}
2146 
2147 				/*
2148 				 * Filter on Path: lines
2149 				 */
2150 				if (arts[i].path && *arts[i].path) {
2151 					if (ptr[j].path != NULL) {
2152 						switch (test_regex(arts[i].path, ptr[j].path, ptr[j].icase, &regex_cache_path[j])) {
2153 							case 1:
2154 								SET_FILTER(group, i, j);
2155 								break;
2156 
2157 							case -1:
2158 								error = TRUE;
2159 								break;
2160 
2161 							default:
2162 								break;
2163 						}
2164 					}
2165 				}
2166 			}
2167 		}
2168 		/*
2169 		 * if (i % (MODULO_COUNT_NUM * 20) == 0)
2170 		 *    show_progress("Filter", i, top_art);
2171 		 */
2172 	}
2173 
2174 	/*
2175 	 * throw away the contents of all regex_caches
2176 	 */
2177 	if (tinrc.wildcard) {
2178 		for (j = 0; j < num; j++) {
2179 			FreeIfNeeded(regex_cache_subj[j].re);
2180 			FreeIfNeeded(regex_cache_subj[j].extra);
2181 			FreeIfNeeded(regex_cache_from[j].re);
2182 			FreeIfNeeded(regex_cache_from[j].extra);
2183 			FreeIfNeeded(regex_cache_msgid[j].re);
2184 			FreeIfNeeded(regex_cache_msgid[j].extra);
2185 			FreeIfNeeded(regex_cache_xref[j].re);
2186 			FreeIfNeeded(regex_cache_xref[j].extra);
2187 			FreeIfNeeded(regex_cache_path[j].re);
2188 			FreeIfNeeded(regex_cache_path[j].extra);
2189 		}
2190 		free(regex_cache_subj);
2191 		free(regex_cache_from);
2192 		free(regex_cache_msgid);
2193 		free(regex_cache_xref);
2194 		free(regex_cache_path);
2195 	}
2196 
2197 	/*
2198 	 * now entering the main filter loop:
2199 	 * all articles have scored, so do kill & select
2200 	 */
2201 	if (!error) {
2202 		for_each_art(i) {
2203 			if (arts[i].score <= tinrc.score_limit_kill) {
2204 				if (arts[i].status == ART_UNREAD)
2205 					arts[i].killed = ART_KILLED_UNREAD;
2206 				else
2207 					arts[i].killed = ART_KILLED;
2208 				filtered = TRUE;
2209 				art_mark(group, &arts[i], ART_READ);
2210 				if (group->attribute->show_only_unread_arts)
2211 					arts[i].keep_in_base = FALSE;
2212 			} else if (arts[i].score >= tinrc.score_limit_select) {
2213 				arts[i].selected = TRUE;
2214 			}
2215 		}
2216 	}
2217 
2218 	if (!cmd_line && !batch_mode)
2219 		clear_message();
2220 
2221 	return filtered;
2222 }
2223 
2224 
2225 /*
2226  * Check if we have to filter on Path: for the given group
2227  */
2228 t_bool
filter_on_path(struct t_group * group)2229 filter_on_path(
2230 	struct t_group *group)
2231 {
2232 	int i;
2233 	struct t_filter *flt;
2234 	t_bool ret = FALSE;
2235 
2236 	if (group->glob_filter->num == 0)
2237 		return ret;
2238 
2239 	if (set_filter_scope(group)) {
2240 		flt = group->glob_filter->filter;
2241 		for (i = 0; i < group->glob_filter->num; i++) {
2242 			if (flt[i].inscope && flt[i].path) {
2243 				ret = TRUE;
2244 				break;
2245 			}
2246 		}
2247 	}
2248 	return ret;
2249 }
2250 
2251 
2252 static int
set_filter_scope(struct t_group * group)2253 set_filter_scope(
2254 	struct t_group *group)
2255 {
2256 	int i, num, inscope;
2257 	struct t_filter *ptr, *prev;
2258 
2259 	inscope = num = group->glob_filter->num;
2260 	prev = ptr = group->glob_filter->filter;
2261 
2262 	for (i = 0; i < num; i++) {
2263 		ptr[i].inscope = TRUE;
2264 		ptr[i].next = (struct t_filter *) 0;
2265 		if (ptr[i].scope != NULL) {
2266 			if (!match_group_list(group->name, ptr[i].scope)) {
2267 				ptr[i].inscope = FALSE;
2268 				inscope--;
2269 			}
2270 		}
2271 		if (i != 0 && ptr[i].inscope)
2272 			prev = prev->next = &ptr[i];
2273 	}
2274 	return inscope;
2275 }
2276 
2277 
2278 /*
2279  * This will come in useful for filtering on non-overview hdr fields
2280  */
2281 #if 0
2282 static FILE *
2283 open_xhdr_fp(
2284 	char *header,
2285 	long min,
2286 	long max)
2287 {
2288 #	ifdef NNTP_ABLE
2289 	if (read_news_via_nntp && !read_saved_news && nntp_caps.hdr_cmd) {
2290 		char buf[NNTP_STRLEN];
2291 
2292 		snprintf(buf, sizeof(buf), "%s %s %ld-%ld", nntp_caps.hdr_cmd, header, min, max);
2293 		return (nntp_command(buf, OK_HEAD, NULL, 0));
2294 	} else
2295 #	endif /* NNTP_ABLE */
2296 		return (FILE *) 0;		/* Some trick implementation for local spool... */
2297 }
2298 #endif /* 0 */
2299