Relicense "GPLv2 or later" files to "GPLv3 or later".
[gnus] / lisp / nnrss.el
1 ;;; nnrss.el --- interfacing with RSS
2
3 ;; Copyright (C) 2001, 2002, 2003, 2004, 2005,
4 ;;   2006, 2007 Free Software Foundation, Inc.
5
6 ;; Author: Shenghuo Zhu <zsh@cs.rochester.edu>
7 ;; Keywords: RSS
8
9 ;; This file is part of GNU Emacs.
10
11 ;; GNU Emacs is free software; you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published
13 ;; by the Free Software Foundation; either version 3, or (at your
14 ;; option) any later version.
15
16 ;; GNU Emacs is distributed in the hope that it will be useful, but
17 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
19 ;; General Public License for more details.
20
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
23 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24 ;; Boston, MA 02110-1301, USA.
25
26 ;;; Commentary:
27
28 ;;; Code:
29
30 (eval-when-compile (require 'cl))
31
32 (require 'gnus)
33 (require 'nnoo)
34 (require 'nnmail)
35 (require 'message)
36 (require 'mm-util)
37 (require 'gnus-util)
38 (require 'time-date)
39 (require 'rfc2231)
40 (require 'mm-url)
41 (require 'rfc2047)
42 (require 'mml)
43 (eval-when-compile
44   (ignore-errors
45    (require 'xml)))
46 (eval '(require 'xml))
47
48 (nnoo-declare nnrss)
49
50 (defvoo nnrss-directory (nnheader-concat gnus-directory "rss/")
51   "Where nnrss will save its files.")
52
53 (defvoo nnrss-ignore-article-fields '(slash:comments)
54   "*List of fields that should be ignored when comparing RSS articles.
55 Some RSS feeds update article fields during their lives, e.g. to
56 indicate the number of comments or the number of times the
57 articles have been seen.  However, if there is a difference
58 between the local article and the distant one, the latter is
59 considered to be new.  To avoid this and discard some fields, set
60 this variable to the list of fields to be ignored.")
61
62 ;; (group max rss-url)
63 (defvoo nnrss-server-data nil)
64
65 ;; (num timestamp url subject author date extra)
66 (defvoo nnrss-group-data nil)
67 (defvoo nnrss-group-max 0)
68 (defvoo nnrss-group-min 1)
69 (defvoo nnrss-group nil)
70 (defvoo nnrss-group-hashtb (make-hash-table :test 'equal))
71 (defvoo nnrss-status-string "")
72
73 (defconst nnrss-version "nnrss 1.0")
74
75 (defvar nnrss-group-alist '()
76   "List of RSS addresses.")
77
78 (defvar nnrss-use-local nil)
79
80 (defvar nnrss-description-field 'X-Gnus-Description
81   "Field name used for DESCRIPTION.
82 To use the description in headers, put this name into `nnmail-extra-headers'.")
83
84 (defvar nnrss-url-field 'X-Gnus-Url
85   "Field name used for URL.
86 To use the description in headers, put this name into `nnmail-extra-headers'.")
87
88 (defvar nnrss-content-function nil
89   "A function which is called in `nnrss-request-article'.
90 The arguments are (ENTRY GROUP ARTICLE).
91 ENTRY is the record of the current headline.  GROUP is the group name.
92 ARTICLE is the article number of the current headline.")
93
94 (defvar nnrss-file-coding-system mm-universal-coding-system
95   "*Coding system used when reading and writing files.
96 If you run Gnus with various versions of Emacsen, the value of this
97 variable should be the coding system that all those Emacsen support.
98 Note that you have to regenerate all the nnrss groups if you change
99 the value.  Moreover, you should be patient even if you are made to
100 read the same articles twice, that arises for the difference of the
101 versions of xml.el.")
102
103 (defvar nnrss-compatible-encoding-alist
104   (delq nil (mapcar (lambda (elem)
105                       (if (and (mm-coding-system-p (car elem))
106                                (mm-coding-system-p (cdr elem)))
107                           elem))
108                     mm-charset-override-alist))
109   "Alist of encodings and those supersets.
110 The cdr of each element is used to decode data if it is available when
111 the car is what the data specify as the encoding.  Or, the car is used
112 for decoding when the cdr that the data specify is not available.")
113
114 (defvar nnrss-wash-html-in-text-plain-parts nil
115   "*Non-nil means render text in text/plain parts as HTML.
116 The function specified by the `mm-text-html-renderer' variable will be
117 used to render text.  If it is nil, text will simply be folded.")
118
119 (nnoo-define-basics nnrss)
120
121 ;;; Interface functions
122
123 (defsubst nnrss-format-string (string)
124   (gnus-replace-in-string string " *\n *" " "))
125
126 (defun nnrss-decode-group-name (group)
127   (if (and group (mm-coding-system-p 'utf-8))
128       (setq group (mm-decode-coding-string group 'utf-8))
129     group))
130
131 (deffoo nnrss-retrieve-headers (articles &optional group server fetch-old)
132   (setq group (nnrss-decode-group-name group))
133   (nnrss-possibly-change-group group server)
134   (let (e)
135     (save-excursion
136       (set-buffer nntp-server-buffer)
137       (erase-buffer)
138       (dolist (article articles)
139         (if (setq e (assq article nnrss-group-data))
140             (insert (number-to-string (car e)) "\t" ;; number
141                     ;; subject
142                     (or (nth 3 e) "")
143                     "\t"
144                     ;; from
145                     (or (nth 4 e) "(nobody)")
146                     "\t"
147                     ;; date
148                     (or (nth 5 e) "")
149                     "\t"
150                     ;; id
151                     (format "<%d@%s.nnrss>" (car e) group)
152                     "\t"
153                     ;; refs
154                     "\t"
155                     ;; chars
156                     "-1" "\t"
157                     ;; lines
158                     "-1" "\t"
159                     ;; Xref
160                     "" "\t"
161                     (if (and (nth 6 e)
162                              (memq nnrss-description-field
163                                    nnmail-extra-headers))
164                         (concat (symbol-name nnrss-description-field)
165                                 ": "
166                                 (nnrss-format-string (nth 6 e))
167                                 "\t")
168                       "")
169                     (if (and (nth 2 e)
170                              (memq nnrss-url-field
171                                    nnmail-extra-headers))
172                         (concat (symbol-name nnrss-url-field)
173                                 ": "
174                                 (nnrss-format-string (nth 2 e))
175                                 "\t")
176                       "")
177                     "\n")))))
178   'nov)
179
180 (deffoo nnrss-request-group (group &optional server dont-check)
181   (setq group (nnrss-decode-group-name group))
182   (nnheader-message 6 "nnrss: Requesting %s..." group)
183   (nnrss-possibly-change-group group server)
184   (prog1
185       (if dont-check
186           t
187         (nnrss-check-group group server)
188         (nnheader-report 'nnrss "Opened group %s" group)
189         (nnheader-insert
190          "211 %d %d %d %s\n" nnrss-group-max nnrss-group-min nnrss-group-max
191          (prin1-to-string group)
192          t))
193     (nnheader-message 6 "nnrss: Requesting %s...done" group)))
194
195 (deffoo nnrss-close-group (group &optional server)
196   t)
197
198 (eval-when-compile
199   (defvar mm-text-html-renderer)
200   (defvar mm-text-html-washer-alist))
201
202 (deffoo nnrss-request-article (article &optional group server buffer)
203   (setq group (nnrss-decode-group-name group))
204   (when (stringp article)
205     (setq article (if (string-match "\\`<\\([0-9]+\\)@" article)
206                       (string-to-number (match-string 1 article))
207                     0)))
208   (nnrss-possibly-change-group group server)
209   (let ((e (assq article nnrss-group-data))
210         (nntp-server-buffer (or buffer nntp-server-buffer))
211         post err)
212     (when e
213       (with-current-buffer nntp-server-buffer
214         (erase-buffer)
215         (if group
216             (insert "Newsgroups: " group "\n"))
217         (if (nth 3 e)
218             (insert "Subject: " (nth 3 e) "\n"))
219         (if (nth 4 e)
220             (insert "From: " (nth 4 e) "\n"))
221         (if (nth 5 e)
222             (insert "Date: " (nnrss-format-string (nth 5 e)) "\n"))
223         (let ((header (buffer-string))
224               (text (nth 6 e))
225               (link (nth 2 e))
226               (enclosure (nth 7 e))
227               (comments (nth 8 e))
228               ;; Enable encoding of Newsgroups header in XEmacs.
229               (default-enable-multibyte-characters t)
230               (rfc2047-header-encoding-alist
231                (if (mm-coding-system-p 'utf-8)
232                    (cons '("Newsgroups" . utf-8)
233                          rfc2047-header-encoding-alist)
234                  rfc2047-header-encoding-alist))
235               rfc2047-encode-encoded-words body fn)
236           (when (or text link enclosure comments)
237             (insert "\n")
238             (insert "<#multipart type=alternative>\n"
239                     "<#part type=\"text/plain\">\n")
240             (setq body (point))
241             (when text
242               (insert text)
243               (goto-char body)
244               (if (and nnrss-wash-html-in-text-plain-parts
245                        (progn
246                          (require 'mm-view)
247                          (setq fn (or (cdr (assq mm-text-html-renderer
248                                                  mm-text-html-washer-alist))
249                                       mm-text-html-renderer))))
250                   (progn
251                     (narrow-to-region body (point-max))
252                     (if (functionp fn)
253                         (funcall fn)
254                       (apply (car fn) (cdr fn)))
255                     (widen)
256                     (goto-char body)
257                     (re-search-forward "[^\t\n ]" nil t)
258                     (beginning-of-line)
259                     (delete-region body (point))
260                     (goto-char (point-max))
261                     (skip-chars-backward "\t\n ")
262                     (end-of-line)
263                     (delete-region (point) (point-max))
264                     (insert "\n"))
265                 (while (re-search-forward "\n+" nil t)
266                   (replace-match " "))
267                 (goto-char body)
268                 ;; See `nnrss-check-group', which inserts "<br /><br />".
269                 (when (search-forward "<br /><br />" nil t)
270                   (if (eobp)
271                       (replace-match "\n")
272                     (replace-match "\n\n")))
273                 (unless (eobp)
274                   (let ((fill-column default-fill-column)
275                         (window (get-buffer-window nntp-server-buffer)))
276                     (when window
277                       (setq fill-column
278                             (max 1 (/ (* (window-width window) 7) 8))))
279                     (fill-region (point) (point-max))
280                     (goto-char (point-max))
281                     ;; XEmacs version of `fill-region' inserts newline.
282                     (unless (bolp)
283                       (insert "\n")))))
284               (when (or link enclosure)
285                 (insert "\n")))
286             (when link
287               (insert link "\n"))
288             (when enclosure
289               (insert (car enclosure) " "
290                       (nth 2 enclosure) " "
291                       (nth 3 enclosure) "\n"))
292             (when comments
293               (insert comments "\n"))
294             (setq body (buffer-substring body (point)))
295             (insert "<#/part>\n"
296                     "<#part type=\"text/html\">\n"
297                     "<html><head></head><body>\n")
298             (when text
299               (insert text "\n"))
300             (when link
301               (insert "<p><a href=\"" link "\">link</a></p>\n"))
302             (when enclosure
303               (insert "<p><a href=\"" (car enclosure) "\">"
304                       (cadr enclosure) "</a> " (nth 2 enclosure)
305                       " " (nth 3 enclosure) "</p>\n"))
306             (when comments
307               (insert "<p><a href=\"" comments "\">comments</a></p>\n"))
308             (insert "</body></html>\n"
309                     "<#/part>\n"
310                     "<#/multipart>\n"))
311           (condition-case nil
312               (mml-to-mime)
313             (error
314              (erase-buffer)
315              (insert header
316                      "Content-Type: text/plain; charset=gnus-decoded\n"
317                      "Content-Transfer-Encoding: 8bit\n\n"
318                      body)
319              (nnheader-message
320               3 "Warning - there might be invalid characters"))))
321         (goto-char (point-min))
322         (search-forward "\n\n")
323         (forward-line -1)
324         (insert (format "Message-ID: <%d@%s.nnrss>\n"
325                         (car e)
326                         (let ((rfc2047-encoding-type 'mime)
327                               rfc2047-encode-max-chars)
328                           (rfc2047-encode-string
329                            (gnus-replace-in-string group "[\t\n ]+" "_")))))
330         (when nnrss-content-function
331           (funcall nnrss-content-function e group article))))
332     (cond
333      (err
334       (nnheader-report 'nnrss err))
335      ((not e)
336       (nnheader-report 'nnrss "no such id: %d" article))
337      (t
338       (nnheader-report 'nnrss "article %s retrieved" (car e))
339       ;; we return the article number.
340       (cons nnrss-group (car e))))))
341
342 (deffoo nnrss-request-list (&optional server)
343   (nnrss-possibly-change-group nil server)
344   (nnrss-generate-active)
345   t)
346
347 (deffoo nnrss-open-server (server &optional defs connectionless)
348   (nnrss-read-server-data server)
349   (nnoo-change-server 'nnrss server defs)
350   t)
351
352 (deffoo nnrss-request-expire-articles
353     (articles group &optional server force)
354   (setq group (nnrss-decode-group-name group))
355   (nnrss-possibly-change-group group server)
356   (let (e days not-expirable changed)
357     (dolist (art articles)
358       (if (and (setq e (assq art nnrss-group-data))
359                (nnmail-expired-article-p
360                 group
361                 (if (listp (setq days (nth 1 e))) days
362                   (days-to-time (- days (time-to-days '(0 0)))))
363                 force))
364           (setq nnrss-group-data (delq e nnrss-group-data)
365                 changed t)
366         (push art not-expirable)))
367     (if changed
368         (nnrss-save-group-data group server))
369     not-expirable))
370
371 (deffoo nnrss-request-delete-group (group &optional force server)
372   (setq group (nnrss-decode-group-name group))
373   (nnrss-possibly-change-group group server)
374   (let (elem)
375     ;; There may be two or more entries in `nnrss-group-alist' since
376     ;; this function didn't delete them formerly.
377     (while (setq elem (assoc group nnrss-group-alist))
378       (setq nnrss-group-alist (delq elem nnrss-group-alist))))
379   (setq nnrss-server-data
380         (delq (assoc group nnrss-server-data) nnrss-server-data))
381   (nnrss-save-server-data server)
382   (ignore-errors
383     (let ((file-name-coding-system nnmail-pathname-coding-system))
384       (delete-file (nnrss-make-filename group server))))
385   t)
386
387 (deffoo nnrss-request-list-newsgroups (&optional server)
388   (nnrss-possibly-change-group nil server)
389   (save-excursion
390     (set-buffer nntp-server-buffer)
391     (erase-buffer)
392     (dolist (elem nnrss-group-alist)
393       (if (third elem)
394           (insert (car elem) "\t" (third elem) "\n"))))
395   t)
396
397 (nnoo-define-skeleton nnrss)
398
399 ;;; Internal functions
400 (eval-when-compile (defun xml-rpc-method-call (&rest args)))
401
402 (defun nnrss-get-encoding ()
403   "Return an encoding attribute specified in the current xml contents.
404 If `nnrss-compatible-encoding-alist' specifies the compatible encoding,
405 it is used instead.  If the xml contents doesn't specify the encoding,
406 return `utf-8' which is the default encoding for xml if it is available,
407 otherwise return nil."
408   (goto-char (point-min))
409   (if (re-search-forward
410        "<\\?[^>]*encoding=\\(?:\"\\([^\">]+\\)\"\\|'\\([^'>]+\\)'\\)"
411        nil t)
412       (let ((encoding (intern (downcase (or (match-string 1)
413                                             (match-string 2))))))
414         (or
415          (mm-coding-system-p (cdr (assq encoding
416                                         nnrss-compatible-encoding-alist)))
417          (mm-coding-system-p encoding)
418          (mm-coding-system-p (car (rassq encoding
419                                          nnrss-compatible-encoding-alist)))))
420     (mm-coding-system-p 'utf-8)))
421
422 (defun nnrss-fetch (url &optional local)
423   "Fetch URL and put it in a the expected Lisp structure."
424   (mm-with-unibyte-buffer
425     ;;some CVS versions of url.el need this to close the connection quickly
426     (let (cs xmlform htmlform)
427       ;; bit o' work necessary for w3 pre-cvs and post-cvs
428       (if local
429           (let ((coding-system-for-read 'binary))
430             (insert-file-contents url))
431         ;; FIXME: shouldn't binding `coding-system-for-read' be moved
432         ;; to `mm-url-insert'?
433         (let ((coding-system-for-read 'binary))
434           (condition-case err
435               (mm-url-insert url)
436             (error (if (or debug-on-quit debug-on-error)
437                        (signal (car err) (cdr err))
438                      (message "nnrss: Failed to fetch %s" url))))))
439       (nnheader-remove-cr-followed-by-lf)
440       ;; Decode text according to the encoding attribute.
441       (when (setq cs (nnrss-get-encoding))
442         (insert (prog1
443                     (mm-decode-coding-string (buffer-string) cs)
444                   (erase-buffer)
445                   (mm-enable-multibyte))))
446       (goto-char (point-min))
447
448       ;; Because xml-parse-region can't deal with anything that isn't
449       ;; xml and w3-parse-buffer can't deal with some xml, we have to
450       ;; parse with xml-parse-region first and, if that fails, parse
451       ;; with w3-parse-buffer.  Yuck.  Eventually, someone should find out
452       ;; why w3-parse-buffer fails to parse some well-formed xml and
453       ;; fix it.
454
455       (condition-case err1
456           (setq xmlform (xml-parse-region (point-min) (point-max)))
457         (error
458          (condition-case err2
459              (setq htmlform (caddar (w3-parse-buffer
460                                      (current-buffer))))
461            (error
462             (message "\
463 nnrss: %s: Not valid XML %s and w3-parse doesn't work %s"
464                      url err1 err2)))))
465       (if htmlform
466           htmlform
467         xmlform))))
468
469 (defun nnrss-possibly-change-group (&optional group server)
470   (when (and server
471              (not (nnrss-server-opened server)))
472     (nnrss-open-server server))
473   (when (and group (not (equal group nnrss-group)))
474     (nnrss-read-group-data group server)
475     (setq nnrss-group group)))
476
477 (defvar nnrss-extra-categories '(nnrss-snarf-moreover-categories))
478
479 (defun nnrss-generate-active ()
480   (when (y-or-n-p "Fetch extra categories? ")
481     (mapc 'funcall nnrss-extra-categories))
482   (save-excursion
483     (set-buffer nntp-server-buffer)
484     (erase-buffer)
485     (dolist (elem nnrss-group-alist)
486       (insert (prin1-to-string (car elem)) " 0 1 y\n"))
487     (dolist (elem nnrss-server-data)
488       (unless (assoc (car elem) nnrss-group-alist)
489         (insert (prin1-to-string (car elem)) " 0 1 y\n")))))
490
491 (eval-and-compile (autoload 'timezone-parse-date "timezone"))
492
493 (defun nnrss-normalize-date (date)
494   "Return a date string of DATE in the RFC822 style.
495 This function handles the ISO 8601 date format described in
496 <URL:http://www.w3.org/TR/NOTE-datetime>, and also the RFC822 style
497 which RSS 2.0 allows."
498   (let (case-fold-search vector year month day time zone cts)
499     (cond ((null date))
500           ;; RFC822
501           ((string-match " [0-9]+ " date)
502            (setq vector (timezone-parse-date date)
503                  year (string-to-number (aref vector 0)))
504            (when (>= year 1969)
505              (setq month (string-to-number (aref vector 1))
506                    day (string-to-number (aref vector 2)))
507              (unless (>= (length (setq time (aref vector 3))) 3)
508                (setq time "00:00:00"))
509              (when (and (setq zone (aref vector 4))
510                         (not (string-match "\\`[A-Z+-]" zone)))
511                (setq zone nil))))
512           ;; ISO 8601
513           ((string-match
514             (eval-when-compile
515               (concat
516                ;; 1. year
517                "\\(199[0-9]\\|20[0-9][0-9]\\)"
518                "\\(?:-"
519                ;; 2. month
520                "\\([01][0-9]\\)"
521                "\\(?:-"
522                ;; 3. day
523                "\\([0-3][0-9]\\)"
524                "\\)?\\)?\\(?:T"
525                ;; 4. hh:mm
526                "\\([012][0-9]:[0-5][0-9]\\)"
527                "\\(?:"
528                ;; 5. :ss
529                "\\(:[0-5][0-9]\\)"
530                "\\(?:\\.[0-9]+\\)?\\)?\\)?"
531                ;; 6+7,8,9. zone
532                "\\(?:\\(?:\\([+-][012][0-9]\\):\\([0-5][0-9]\\)\\)"
533                "\\|\\([+-][012][0-9][0-5][0-9]\\)"
534                "\\|\\(Z\\)\\)?"))
535             date)
536            (setq year (string-to-number (match-string 1 date))
537                  month (string-to-number (or (match-string 2 date) "1"))
538                  day (string-to-number (or (match-string 3 date) "1"))
539                  time (if (match-beginning 5)
540                           (substring date (match-beginning 4) (match-end 5))
541                         (concat (or (match-string 4 date) "00:00") ":00"))
542                  zone (cond ((match-beginning 6)
543                              (concat (match-string 6 date)
544                                      (match-string 7 date)))
545                             ((match-beginning 9) ;; Z
546                              "+0000")
547                             (t ;; nil if zone is not provided.
548                              (match-string 8 date))))))
549     (if month
550         (progn
551           (setq cts (current-time-string (encode-time 0 0 0 day month year)))
552           (format "%s, %02d %s %04d %s%s"
553                   (substring cts 0 3) day (substring cts 4 7) year time
554                   (if zone
555                       (concat " " zone)
556                     "")))
557       (message-make-date))))
558
559 ;;; data functions
560
561 (defun nnrss-read-server-data (server)
562   (setq nnrss-server-data nil)
563   (let ((file (nnrss-make-filename "nnrss" server))
564         (file-name-coding-system nnmail-pathname-coding-system))
565     (when (file-exists-p file)
566       ;; In Emacs 21.3 and earlier, `load' doesn't support non-ASCII
567       ;; file names.  So, we use `insert-file-contents' instead.
568       (mm-with-multibyte-buffer
569         (let ((coding-system-for-read nnrss-file-coding-system))
570           (insert-file-contents file)
571           (eval-region (point-min) (point-max)))))))
572
573 (defun nnrss-save-server-data (server)
574   (gnus-make-directory nnrss-directory)
575   (let ((coding-system-for-write nnrss-file-coding-system)
576         (file-name-coding-system nnmail-pathname-coding-system))
577     (with-temp-file (nnrss-make-filename "nnrss" server)
578       (insert (format ";; -*- coding: %s; -*-\n"
579                       nnrss-file-coding-system))
580       (gnus-prin1 `(setq nnrss-group-alist ',nnrss-group-alist))
581       (insert "\n")
582       (gnus-prin1 `(setq nnrss-server-data ',nnrss-server-data)))))
583
584 (defun nnrss-read-group-data (group server)
585   (setq nnrss-group-data nil)
586   (if (hash-table-p nnrss-group-hashtb)
587       (clrhash nnrss-group-hashtb)
588     (setq nnrss-group-hashtb (make-hash-table :test 'equal)))
589   (let ((pair (assoc group nnrss-server-data)))
590     (setq nnrss-group-max (or (cadr pair) 0))
591     (setq nnrss-group-min (+ nnrss-group-max 1)))
592   (let ((file (nnrss-make-filename group server))
593         (file-name-coding-system nnmail-pathname-coding-system))
594     (when (file-exists-p file)
595       ;; In Emacs 21.3 and earlier, `load' doesn't support non-ASCII
596       ;; file names.  So, we use `insert-file-contents' instead.
597       (mm-with-multibyte-buffer
598         (let ((coding-system-for-read nnrss-file-coding-system))
599           (insert-file-contents file)
600           (eval-region (point-min) (point-max))))
601       (dolist (e nnrss-group-data)
602         (puthash (nth 9 e) t nnrss-group-hashtb)
603         (when (and (car e) (> nnrss-group-min (car e)))
604           (setq nnrss-group-min (car e)))
605         (when (and (car e) (< nnrss-group-max (car e)))
606           (setq nnrss-group-max (car e)))))))
607
608 (defun nnrss-save-group-data (group server)
609   (gnus-make-directory nnrss-directory)
610   (let ((coding-system-for-write nnrss-file-coding-system)
611         (file-name-coding-system nnmail-pathname-coding-system))
612     (with-temp-file (nnrss-make-filename group server)
613       (insert (format ";; -*- coding: %s; -*-\n"
614                       nnrss-file-coding-system))
615       (gnus-prin1 `(setq nnrss-group-data ',nnrss-group-data)))))
616
617 (defun nnrss-make-filename (name server)
618   (expand-file-name
619    (nnrss-translate-file-chars
620     (concat name
621             (and server
622                  (not (equal server ""))
623                  "-")
624             server
625             ".el"))
626    nnrss-directory))
627
628 (gnus-add-shutdown 'nnrss-close 'gnus)
629
630 (defun nnrss-close ()
631   "Clear internal nnrss variables."
632   (setq nnrss-group-data nil
633         nnrss-server-data nil
634         nnrss-group-hashtb nil
635         nnrss-group-alist nil))
636
637 ;;; URL interface
638
639 (defun nnrss-no-cache (url)
640   "")
641
642 (defun nnrss-insert-w3 (url)
643   (mm-with-unibyte-current-buffer
644     (condition-case err
645         (mm-url-insert url)
646       (error (if (or debug-on-quit debug-on-error)
647                  (signal (car err) (cdr err))
648                (message "nnrss: Failed to fetch %s" url))))))
649
650 (defun nnrss-decode-entities-string (string)
651   (if string
652       (mm-with-multibyte-buffer
653         (insert string)
654         (mm-url-decode-entities-nbsp)
655         (buffer-string))))
656
657 (defalias 'nnrss-insert 'nnrss-insert-w3)
658
659 (defun nnrss-mime-encode-string (string)
660   (mm-with-multibyte-buffer
661     (insert string)
662     (mm-url-decode-entities-nbsp)
663     (goto-char (point-min))
664     (while (re-search-forward "[\t\n ]+" nil t)
665       (replace-match " "))
666     (goto-char (point-min))
667     (skip-chars-forward " ")
668     (delete-region (point-min) (point))
669     (goto-char (point-max))
670     (skip-chars-forward " ")
671     (delete-region (point) (point-max))
672     (let ((rfc2047-encoding-type 'mime)
673           rfc2047-encode-max-chars)
674       (rfc2047-encode-region (point-min) (point-max)))
675     (goto-char (point-min))
676     (while (search-forward "\n" nil t)
677       (delete-backward-char 1))
678     (buffer-string)))
679
680 ;;; Snarf functions
681
682 (defun nnrss-make-hash-index (item)
683   (setq item (remove-if
684               (lambda (field)
685                 (when (listp field)
686                   (memq (car field) nnrss-ignore-article-fields)))
687               item))
688   (md5 (gnus-prin1-to-string item)
689        nil nil
690        nnrss-file-coding-system))
691
692 (defun nnrss-check-group (group server)
693   (let (file xml subject url extra changed author date feed-subject
694              enclosure comments rss-ns rdf-ns content-ns dc-ns
695              hash-index)
696     (if (and nnrss-use-local
697              (file-exists-p (setq file (expand-file-name
698                                         (nnrss-translate-file-chars
699                                          (concat group ".xml"))
700                                         nnrss-directory))))
701         (setq xml (nnrss-fetch file t))
702       (setq url (or (nth 2 (assoc group nnrss-server-data))
703                     (second (assoc group nnrss-group-alist))))
704       (unless url
705         (setq url
706               (cdr
707                (assoc 'href
708                       (nnrss-discover-feed
709                        (read-string
710                         (format "URL to search for %s: " group) "http://")))))
711         (let ((pair (assoc group nnrss-server-data)))
712           (if pair
713               (setcdr (cdr pair) (list url))
714             (push (list group nnrss-group-max url) nnrss-server-data)))
715         (setq changed t))
716       (setq xml (nnrss-fetch url)))
717     ;; See
718     ;; http://feeds.archive.org/validator/docs/howto/declare_namespaces.html
719     ;; for more RSS namespaces.
720     (setq dc-ns (nnrss-get-namespace-prefix xml "http://purl.org/dc/elements/1.1/")
721           rdf-ns (nnrss-get-namespace-prefix xml "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
722           rss-ns (nnrss-get-namespace-prefix xml "http://purl.org/rss/1.0/")
723           content-ns (nnrss-get-namespace-prefix xml "http://purl.org/rss/1.0/modules/content/"))
724     (dolist (item (nreverse (nnrss-find-el (intern (concat rss-ns "item")) xml)))
725       (when (and (listp item)
726                  (string= (concat rss-ns "item") (car item))
727                  (progn (setq hash-index (nnrss-make-hash-index item))
728                         (not (gethash hash-index nnrss-group-hashtb))))
729         (setq subject (nnrss-node-text rss-ns 'title item))
730         (setq url (nnrss-decode-entities-string
731                    (nnrss-node-text rss-ns 'link (cddr item))))
732         (setq extra (or (nnrss-node-text content-ns 'encoded item)
733                         (nnrss-node-text rss-ns 'description item)))
734         (if (setq feed-subject (nnrss-node-text dc-ns 'subject item))
735             (setq extra (concat feed-subject "<br /><br />" extra)))
736         (setq author (or (nnrss-node-text rss-ns 'author item)
737                          (nnrss-node-text dc-ns 'creator item)
738                          (nnrss-node-text dc-ns 'contributor item)))
739         (setq date (nnrss-normalize-date
740                     (or (nnrss-node-text dc-ns 'date item)
741                         (nnrss-node-text rss-ns 'pubDate item))))
742         (setq comments (nnrss-node-text rss-ns 'comments item))
743         (when (setq enclosure (cadr (assq (intern (concat rss-ns "enclosure")) item)))
744           (let ((url (cdr (assq 'url enclosure)))
745                 (len (cdr (assq 'length enclosure)))
746                 (type (cdr (assq 'type enclosure)))
747                 (name))
748             (setq len
749                   (if (and len (integerp (setq len (string-to-number len))))
750                       ;; actually already in `ls-lisp-format-file-size' but
751                       ;; probably not worth to require it for one function
752                       (do ((size (/ len 1.0) (/ size 1024.0))
753                            (post-fixes (list "" "k" "M" "G" "T" "P" "E")
754                                        (cdr post-fixes)))
755                           ((< size 1024)
756                            (format "%.1f%s" size (car post-fixes))))
757                     "0"))
758             (setq url (or url ""))
759             (setq name (if (string-match "/\\([^/]*\\)$" url)
760                            (match-string 1 url)
761                          "file"))
762             (setq type (or type ""))
763             (setq enclosure (list url name len type))))
764         (push
765          (list
766           (incf nnrss-group-max)
767           (current-time)
768           url
769           (and subject (nnrss-mime-encode-string subject))
770           (and author (nnrss-mime-encode-string author))
771           date
772           (and extra (nnrss-decode-entities-string extra))
773           enclosure
774           comments
775           hash-index)
776          nnrss-group-data)
777         (puthash hash-index t nnrss-group-hashtb)
778         (setq changed t))
779       (setq extra nil))
780     (when changed
781       (nnrss-save-group-data group server)
782       (let ((pair (assoc group nnrss-server-data)))
783         (if pair
784             (setcar (cdr pair) nnrss-group-max)
785           (push (list group nnrss-group-max) nnrss-server-data)))
786       (nnrss-save-server-data server))))
787
788 (defun nnrss-opml-import (opml-file)
789   "OPML subscriptions import.
790 Read the file and attempt to subscribe to each Feed in the file."
791   (interactive "fImport file: ")
792   (mapc
793    (lambda (node)
794      (let ((xmlurl (cdr (assq 'xmlUrl (cadr node)))))
795        (when (and xmlurl
796                   (not (string-match "\\`[\t ]*\\'" xmlurl))
797                   (prog1
798                       (y-or-n-p (format "Subscribe to %s " xmlurl))
799                     (message "")))
800          (condition-case err
801              (progn
802                (gnus-group-make-rss-group xmlurl)
803                (forward-line 1))
804            (error
805             (message
806              "Failed to subscribe to %s (%s); type any key to continue: "
807              xmlurl
808              (error-message-string err))
809             (let ((echo-keystrokes 0))
810               (read-char)))))))
811    (nnrss-find-el 'outline
812                   (mm-with-multibyte-buffer
813                     (insert-file-contents opml-file)
814                     (xml-parse-region (point-min) (point-max))))))
815
816 (defun nnrss-opml-export ()
817   "OPML subscription export.
818 Export subscriptions to a buffer in OPML Format."
819   (interactive)
820   (with-current-buffer (get-buffer-create "*OPML Export*")
821     (mm-set-buffer-file-coding-system 'utf-8)
822     (insert "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
823             "<!-- OPML generated by Emacs Gnus' nnrss.el -->\n"
824             "<opml version=\"1.1\">\n"
825             "  <head>\n"
826             "    <title>mySubscriptions</title>\n"
827             "    <dateCreated>" (format-time-string "%a, %d %b %Y %T %z")
828             "</dateCreated>\n"
829             "    <ownerEmail>" user-mail-address "</ownerEmail>\n"
830             "    <ownerName>" (user-full-name) "</ownerName>\n"
831             "  </head>\n"
832             "  <body>\n")
833     (dolist (sub nnrss-group-alist)
834       (insert "    <outline text=\"" (car sub)
835               "\" xmlUrl=\"" (cadr sub) "\"/>\n"))
836     (insert "  </body>\n"
837             "</opml>\n"))
838   (pop-to-buffer "*OPML Export*")
839   (when (fboundp 'sgml-mode)
840     (sgml-mode)))
841
842 (defun nnrss-generate-download-script ()
843   "Generate a download script in the current buffer.
844 It is useful when `(setq nnrss-use-local t)'."
845   (interactive)
846   (insert "#!/bin/sh\n")
847   (insert "WGET=wget\n")
848   (insert "RSSDIR='" (expand-file-name nnrss-directory) "'\n")
849   (dolist (elem nnrss-server-data)
850     (let ((url (or (nth 2 elem)
851                    (second (assoc (car elem) nnrss-group-alist)))))
852       (insert "$WGET -q -O \"$RSSDIR\"/'"
853               (nnrss-translate-file-chars (concat (car elem) ".xml"))
854               "' '" url "'\n"))))
855
856 (defun nnrss-translate-file-chars (name)
857   (let ((nnheader-file-name-translation-alist
858          (append nnheader-file-name-translation-alist '((?' . ?_)))))
859     (nnheader-translate-file-chars name)))
860
861 (defvar nnrss-moreover-url
862   "http://w.moreover.com/categories/category_list_rss.html"
863   "The url of moreover.com categories.")
864
865 (defun nnrss-snarf-moreover-categories ()
866   "Snarf RSS links from moreover.com."
867   (interactive)
868   (let (category name url changed)
869     (with-temp-buffer
870       (nnrss-insert nnrss-moreover-url)
871       (goto-char (point-min))
872       (while (re-search-forward
873               "<a name=\"\\([^\"]+\\)\">\\|<a href=\"\\(http://[^\"]*moreover\\.com[^\"]+page\\?c=\\([^\"&]+\\)&o=rss\\)" nil t)
874         (if (match-string 1)
875             (setq category (match-string 1))
876           (setq url (match-string 2)
877                 name (mm-url-decode-entities-string
878                       (rfc2231-decode-encoded-string
879                        (match-string 3))))
880           (if category
881               (setq name (concat category "." name)))
882           (unless (assoc name nnrss-server-data)
883             (setq changed t)
884             (push (list name 0 url) nnrss-server-data)))))
885     (if changed
886         (nnrss-save-server-data ""))))
887
888 (defun nnrss-node-text (namespace local-name element)
889   (let* ((node (assq (intern (concat namespace (symbol-name local-name)))
890                      element))
891          (text (if (and node (listp node))
892                    (nnrss-node-just-text node)
893                  node))
894          (cleaned-text (if text
895                            (gnus-replace-in-string
896                             (gnus-replace-in-string
897                              text "^[\000-\037\177]+\\|^ +\\| +$" "")
898                             "\r\n" "\n"))))
899     (if (string-equal "" cleaned-text)
900         nil
901       cleaned-text)))
902
903 (defun nnrss-node-just-text (node)
904   (if (and node (listp node))
905       (mapconcat 'nnrss-node-just-text (cddr node) " ")
906     node))
907
908 (defun nnrss-find-el (tag data &optional found-list)
909   "Find the all matching elements in the data.
910 Careful with this on large documents!"
911   (when (consp data)
912     (dolist (bit data)
913       (when (car-safe bit)
914         (when (equal tag (car bit))
915           ;; Old xml.el may return a list of string.
916           (when (and (consp (caddr bit))
917                      (stringp (caaddr bit)))
918             (setcar (cddr bit) (caaddr bit)))
919           (setq found-list
920                 (append found-list
921                         (list bit))))
922         (if (and (consp (car-safe (caddr bit)))
923                  (not (stringp (caddr bit))))
924             (setq found-list
925                   (append found-list
926                           (nnrss-find-el
927                            tag (caddr bit))))
928           (setq found-list
929                 (append found-list
930                         (nnrss-find-el
931                          tag (cddr bit))))))))
932   found-list)
933
934 (defun nnrss-rsslink-p (el)
935   "Test if the element we are handed is an RSS autodiscovery link."
936   (and (eq (car-safe el) 'link)
937        (string-equal (cdr (assoc 'rel (cadr el))) "alternate")
938        (or (string-equal (cdr (assoc 'type (cadr el)))
939                          "application/rss+xml")
940            (string-equal (cdr (assoc 'type (cadr el))) "text/xml"))))
941
942 (defun nnrss-get-rsslinks (data)
943   "Extract the <link> elements that are links to RSS from the parsed data."
944   (delq nil (mapcar
945              (lambda (el)
946                (if (nnrss-rsslink-p el) el))
947              (nnrss-find-el 'link data))))
948
949 (defun nnrss-extract-hrefs (data)
950   "Recursively extract hrefs from a page's source.
951 DATA should be the output of `xml-parse-region' or
952 `w3-parse-buffer'."
953   (mapcar (lambda (ahref)
954             (cdr (assoc 'href (cadr ahref))))
955           (nnrss-find-el 'a data)))
956
957 (defmacro nnrss-match-macro (base-uri item onsite-list offsite-list)
958   `(cond ((or (string-match (concat "^" ,base-uri) ,item)
959               (not (string-match "://" ,item)))
960           (setq ,onsite-list (append ,onsite-list (list ,item))))
961          (t (setq ,offsite-list (append ,offsite-list (list ,item))))))
962
963 (defun nnrss-order-hrefs (base-uri hrefs)
964   "Given a list of hrefs, sort them using the following priorities:
965   1. links ending in .rss
966   2. links ending in .rdf
967   3. links ending in .xml
968   4. links containing the above
969   5. offsite links
970
971 BASE-URI is used to determine the location of the links and
972 whether they are `offsite' or `onsite'."
973   (let (rss-onsite-end  rdf-onsite-end  xml-onsite-end
974         rss-onsite-in   rdf-onsite-in   xml-onsite-in
975         rss-offsite-end rdf-offsite-end xml-offsite-end
976         rss-offsite-in rdf-offsite-in xml-offsite-in)
977     (dolist (href hrefs)
978       (cond ((null href))
979             ((string-match "\\.rss$" href)
980              (nnrss-match-macro
981               base-uri href rss-onsite-end rss-offsite-end))
982             ((string-match "\\.rdf$" href)
983              (nnrss-match-macro
984               base-uri href rdf-onsite-end rdf-offsite-end))
985             ((string-match "\\.xml$" href)
986              (nnrss-match-macro
987               base-uri href xml-onsite-end xml-offsite-end))
988             ((string-match "rss" href)
989              (nnrss-match-macro
990               base-uri href rss-onsite-in rss-offsite-in))
991             ((string-match "rdf" href)
992              (nnrss-match-macro
993               base-uri href rdf-onsite-in rdf-offsite-in))
994             ((string-match "xml" href)
995              (nnrss-match-macro
996               base-uri href xml-onsite-in xml-offsite-in))))
997     (append
998      rss-onsite-end  rdf-onsite-end  xml-onsite-end
999      rss-onsite-in   rdf-onsite-in   xml-onsite-in
1000      rss-offsite-end rdf-offsite-end xml-offsite-end
1001      rss-offsite-in rdf-offsite-in xml-offsite-in)))
1002
1003 (defun nnrss-discover-feed (url)
1004   "Given a page, find an RSS feed using Mark Pilgrim's
1005 `ultra-liberal rss locator' (http://diveintomark.org/2002/08/15.html)."
1006
1007   (let ((parsed-page (nnrss-fetch url)))
1008
1009 ;;    1. if this url is the rss, use it.
1010     (if (nnrss-rss-p parsed-page)
1011         (let ((rss-ns (nnrss-get-namespace-prefix parsed-page "http://purl.org/rss/1.0/")))
1012           (nnrss-rss-title-description rss-ns parsed-page url))
1013
1014 ;;    2. look for the <link rel="alternate"
1015 ;;    type="application/rss+xml" and use that if it is there.
1016       (let ((links (nnrss-get-rsslinks parsed-page)))
1017         (if links
1018             (let* ((xml (nnrss-fetch
1019                          (cdr (assoc 'href (cadar links)))))
1020                    (rss-ns (nnrss-get-namespace-prefix xml "http://purl.org/rss/1.0/")))
1021               (nnrss-rss-title-description rss-ns xml (cdr (assoc 'href (cadar links)))))
1022
1023 ;;    3. look for links on the site in the following order:
1024 ;;       - onsite links ending in .rss, .rdf, or .xml
1025 ;;       - onsite links containing any of the above
1026 ;;       - offsite links ending in .rss, .rdf, or .xml
1027 ;;       - offsite links containing any of the above
1028           (let* ((base-uri (progn (string-match ".*://[^/]+/?" url)
1029                                   (match-string 0 url)))
1030                  (hrefs (nnrss-order-hrefs
1031                          base-uri (nnrss-extract-hrefs parsed-page)))
1032                  (rss-link nil))
1033             (while (and (eq rss-link nil) (not (eq hrefs nil)))
1034               (let ((href-data (nnrss-fetch (car hrefs))))
1035                 (if (nnrss-rss-p href-data)
1036                     (let* ((rss-ns (nnrss-get-namespace-prefix href-data "http://purl.org/rss/1.0/")))
1037                       (setq rss-link (nnrss-rss-title-description
1038                                       rss-ns href-data (car hrefs))))
1039                   (setq hrefs (cdr hrefs)))))
1040             (if rss-link rss-link
1041
1042 ;;    4. check syndic8
1043               (nnrss-find-rss-via-syndic8 url))))))))
1044
1045 (defun nnrss-find-rss-via-syndic8 (url)
1046   "Query syndic8 for the rss feeds it has for URL."
1047   (if (not (locate-library "xml-rpc"))
1048       (progn
1049         (message "XML-RPC is not available... not checking Syndic8.")
1050         nil)
1051     (require 'xml-rpc)
1052     (let ((feedid (xml-rpc-method-call
1053                    "http://www.syndic8.com/xmlrpc.php"
1054                    'syndic8.FindSites
1055                    url)))
1056       (when feedid
1057         (let* ((feedinfo (xml-rpc-method-call
1058                           "http://www.syndic8.com/xmlrpc.php"
1059                           'syndic8.GetFeedInfo
1060                           feedid))
1061                (urllist
1062                 (delq nil
1063                       (mapcar
1064                        (lambda (listinfo)
1065                          (if (string-equal
1066                               (cdr (assoc "status" listinfo))
1067                               "Syndicated")
1068                              (cons
1069                               (cdr (assoc "sitename" listinfo))
1070                               (list
1071                                (cons 'title
1072                                      (cdr (assoc
1073                                            "sitename" listinfo)))
1074                                (cons 'href
1075                                      (cdr (assoc
1076                                            "dataurl" listinfo)))))))
1077                        feedinfo))))
1078           (if (not (> (length urllist) 1))
1079               (cdar urllist)
1080             (let ((completion-ignore-case t)
1081                   (selection
1082                    (mapcar (lambda (listinfo)
1083                              (cons (cdr (assoc "sitename" listinfo))
1084                                    (string-to-number
1085                                     (cdr (assoc "feedid" listinfo)))))
1086                            feedinfo)))
1087               (cdr (assoc
1088                     (completing-read
1089                      "Multiple feeds found.  Select one: "
1090                      selection nil t) urllist)))))))))
1091
1092 (defun nnrss-rss-p (data)
1093   "Test if DATA is an RSS feed.
1094 Simply ensures that the first element is rss or rdf."
1095   (or (eq (caar data) 'rss)
1096       (eq (caar data) 'rdf:RDF)))
1097
1098 (defun nnrss-rss-title-description (rss-namespace data url)
1099   "Return the title of an RSS feed."
1100   (if (nnrss-rss-p data)
1101       (let ((description (intern (concat rss-namespace "description")))
1102             (title (intern (concat rss-namespace "title")))
1103             (channel (nnrss-find-el (intern (concat rss-namespace "channel"))
1104                                     data)))
1105         (list
1106          (cons 'description (caddr (nth 0 (nnrss-find-el description channel))))
1107          (cons 'title (caddr (nth 0 (nnrss-find-el title channel))))
1108          (cons 'href url)))))
1109
1110 (defun nnrss-get-namespace-prefix (el uri)
1111   "Given EL (containing a parsed element) and URI (containing a string
1112 that gives the URI for which you want to retrieve the namespace
1113 prefix), return the prefix."
1114   (let* ((prefix (car (rassoc uri (cadar el))))
1115          (nslist (if prefix
1116                      (split-string (symbol-name prefix) ":")))
1117          (ns (cond ((eq (length nslist) 1) ; no prefix given
1118                     "")
1119                    ((eq (length nslist) 2) ; extract prefix
1120                     (cadr nslist)))))
1121     (if (and ns (not (string= ns "")))
1122         (concat ns ":")
1123       ns)))
1124
1125 (provide 'nnrss)
1126
1127
1128 ;;; nnrss.el ends here
1129
1130 ;;; arch-tag: 12910c07-0cdf-44fb-8d2c-416ded64c267