source: project/release/4/qwiki/trunk/qwiki.scm @ 15464

Last change on this file since 15464 was 15464, checked in by sjamaan, 10 years ago

Add internal wiki link rules and allow pretty urls

File size: 13.1 KB
Line 
1;;
2;; qwiki - the quick wiki
3;;
4;; Copyright (c) 2009 Peter Bex and Ivan Raikov
5;;
6;;  Redistribution and use in source and binary forms, with or without
7;;  modification, are permitted provided that the following conditions
8;;  are met:
9;;
10;;  - Redistributions of source code must retain the above copyright
11;;  notice, this list of conditions and the following disclaimer.
12;;
13;;  - Redistributions in binary form must reproduce the above
14;;  copyright notice, this list of conditions and the following
15;;  disclaimer in the documentation and/or other materials provided
16;;  with the distribution.
17;;
18;;  - Neither name of the copyright holders nor the names of its
19;;  contributors may be used to endorse or promote products derived
20;;  from this software without specific prior written permission.
21;;
22;;  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND THE
23;;  CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
24;;  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
25;;  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26;;  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR THE
27;;  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
28;;  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
29;;  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
30;;  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
31;;  AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
32;;  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
33;;  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34;;  POSSIBILITY OF SUCH DAMAGE.
35
36(module qwiki
37  (qwiki-docroot 
38   qwiki-source-path 
39   qwiki-base-uri 
40   qwiki-handler
41   qwiki-show
42   qwiki-edit
43   qwiki-history
44   qwiki-render-file
45   qwiki-transformation-steps
46   qwiki-extensions
47   )
48
49(import chicken scheme)
50(use extras files posix ports data-structures srfi-1 srfi-13 srfi-14
51     intarweb uri-common spiffy sxml-transforms
52     wiki-parse qwiki-sxml doctype sxml-fu sxml-shortcuts
53     ;; There should be a way to parameterize the versioning implementation
54     qwiki-svn)
55
56;; HTML files are stored here, relative to the current Spiffy docroot
57(define qwiki-docroot (make-parameter "/"))
58
59;; The location of the wiki source files (where a checkout will be made)
60(define qwiki-source-path (make-parameter "/tmp/qwiki"))
61
62;; The base URI for this wiki
63(define qwiki-base-uri (make-parameter "/" uri-reference))
64 
65;; The rules used for rendering wiki pages (default is HTML)
66(define qwiki-output-driver
67  (make-parameter qwiki-html-transformation-rules))
68
69(define qwiki-extensions
70  (make-parameter (list)))
71
72;; This must match name-to-base in svnwiki/deps.scm
73;; It is changed slightly to disallow newlines, tabs or other "weird"
74;; whitespace characters.
75(define (simplify-pagename pagename)
76  (if (file-exists? (make-pathname (qwiki-source-path) pagename))
77      pagename
78      (string-downcase
79       (string-filter (char-set-union char-set:letter+digit
80                                      (char-set #\space #\/ #\-))
81                      (string-translate pagename " " "-")))))
82
83(define wiki-link-normalization
84  `((wiki . ,(lambda (tag href . contents)
85               (let ((pretty-href (simplify-pagename href)))
86                 (if (pair? contents)
87                     `(wiki ,pretty-href ,@contents)
88                     `(wiki ,pretty-href ,href)))))
89    ,@alist-conv-rules))
90
91;; The rules used for transforming page SXML structure
92(define (qwiki-transformation-steps content)
93  (append (list wiki-link-normalization)
94          (qwiki-extensions)
95          ((qwiki-output-driver) content)
96          ))
97
98;; The basic template for SXML wiki pages
99(define (qwiki-sxml-page-template contents . headers)
100   `(wiki-page (Header ,@headers)
101               (body (page-specific-links)
102                     ,contents)))
103
104;; Return the trailing part of the path relative to the docroot/base-uri
105;; eg: If the wiki lives under /qwiki, /qwiki/eggref/4/9p gives /eggref/4/9p
106(define (relative-uri-path uri)
107  ;; Both URIs are assumed to contain absolute paths
108  (let loop ((path (cdr (uri-path uri)))
109             (base-path (cdr (uri-path (qwiki-base-uri)))))
110    (cond
111     ((or (null? base-path) (string-null? (car base-path))) path)
112     ((and (not (null? path))
113           (string=? (car path) (car base-path)))
114      (loop (cdr path) (cdr base-path)))
115     (else (error "Bad request URI path. Please configure qwiki-base-uri.")))))
116
117(define (path->html-filename path)
118  (make-pathname (qwiki-docroot)
119                 (string-join path "/") "html"))
120
121(define (path->source-filename path)
122  (make-pathname (qwiki-source-path) (string-join path "/")))
123
124;; Handle index files where needed.  Never try to open a directory as file
125(define (normalize-path path)
126  (if (directory? (path->source-filename path))
127      (append path '("index"))
128      path))
129
130;; Like with-output-to-file, only this creates parent directories as needed.
131(define (with-output-to-path path thunk)
132  (unless (file-exists? (pathname-directory path))
133    (create-directory (pathname-directory path) #t))
134  (with-output-to-file path thunk))
135
136(define (send-content content)
137  (write-logged-response)
138  (with-output-to-port (response-port (current-response))
139    (lambda ()
140      (output-xml content (qwiki-transformation-steps content))))
141  (close-output-port (response-port (current-response))))
142
143
144
145;;; Actions
146(define (qwiki-history path req)
147  (let* ((source-file (path->source-filename path))
148         (rev (string->number
149               (alist-ref 'rev (uri-query (request-uri req)) eq? "")))
150         (history (get-history source-file rev #f)) ; no pagination yet
151         (content (qwiki-sxml-page-template `(history ,history))))
152    (send-content content)))
153
154(define (qwiki-edit path req)
155  (let* ((html-file (path->html-filename path))
156         (source-file (path->source-filename path))
157         (postdata (if (eq? 'POST (request-method req))
158                       (form-urldecode (read-request-data req))
159                       '()))
160         (source (or (alist-ref 'source postdata)
161                     (and (file-exists? source-file)
162                          (with-input-from-file source-file read-string))
163                     ""))
164         (comment (alist-ref 'comment postdata eq? ""))
165         (username (alist-ref 'username postdata eq? ""))
166         (password (alist-ref 'password postdata eq? ""))
167         (auth (alist-ref 'auth postdata eq?))
168         ;; TODO: Clean this up, maybe put it in a transformation rule so
169         ;; it can be extended by plugins.  The names of the buttons are
170         ;; pretty much tied to the code though
171         (make-form
172          (lambda (#!optional message)
173            (qwiki-sxml-page-template 
174             `(,(if (alist-ref 'preview postdata)
175                    `(div (@ (class "preview"))
176                          (h2 "Preview")
177                          ,(wiki-parse source))
178                    "")
179               ,(if message
180                    `(div (@ class "message") ,message)
181                    "")
182               (form (@ (method "post") (action ""))
183                     (div (@ (id "article"))
184                          (label "Article contents:"
185                                 (textarea (@ (name "source")
186                                              (rows "20") (cols "72"))
187                                           ,source))
188                          (label "Description of your changes:"
189                                 (textarea (@ (name "comment")
190                                              (rows "2") (cols "72"))
191                                           ,comment)))
192                     (div (@ (id "auth"))
193                          (label "I would like to authenticate"
194                                 (input (@ (type "checkbox")
195                                           (name "auth")
196                                           ,@(if auth
197                                                 '((checked "checked"))
198                                                 '()))))
199                          (label "Username:"
200                                 (input (@ (type "text")
201                                           (name "username")
202                                           (value ,username))))
203                          (label "Password:"
204                                 (input (@ (type "password")
205                                           (name "password")
206                                           (value ,password)))))
207                     (div (@ (id "actions"))
208                          (input (@ (type "submit")
209                                    (name "save")
210                                    (value "Save")))
211                          (input (@ (type "submit")
212                                    (name "preview")
213                                    (value "Preview"))))))))))
214    (if (alist-ref 'save postdata)
215        (begin
216          (with-output-to-path source-file (lambda () (display source)))
217          (handle-exceptions exn
218            (begin
219              (undo-changes! source-file)
220              (update-sources! source-file)
221              (send-content (make-form (conc "Warning! Someone has edited this page while you were editing it. You can click save again to overwrite those changes with yours if this is the case."
222                                             (if auth
223                                                 " It is also possible your username/password are incorrect."
224                                                 "")))))
225            (store-changes! source-file comment
226                            (and auth username) (and auth password))
227            (redirect-to-qwiki-page req action: "show")))
228        (send-content (make-form)))))
229
230(define (redirect-to-qwiki-page req
231                                #!key
232                                ;; TODO: make path relative to qwiki-base-uri
233                                (path (uri-path (request-uri req)))
234                                (action "show"))
235  (with-headers `((location
236                   ,(update-uri (server-root-uri)
237                                path: path
238                                query: (alist-update!
239                                        'action action
240                                        (or (uri-query (request-uri req))
241                                            '())))))
242    ;; Maybe send a 303?
243    (lambda () (send-status 302 "Found"))))
244
245(define (qwiki-show path req)
246  ;; TODO: What if someone did something else than GET or HEAD?
247  (let* ((html-file (path->html-filename path))
248         (source-file (path->source-filename path))
249         (rev (string->number
250               (alist-ref 'rev (uri-query (request-uri req)) eq? ""))))
251    (if (file-exists? source-file)
252        (if rev
253            (send-content ; Do not store if old rev
254             (qwiki-sxml-page-template
255              (call-with-input-revision
256               source-file rev wiki-parse)))
257            (begin
258             (update-html-file! (make-pathname (root-path) html-file)
259                                source-file)
260             (send-static-file html-file)))
261        (redirect-to-qwiki-page req action: "edit"))))
262
263(define (file-newer? a b)
264  (> (file-modification-time a) (file-modification-time b)))
265
266;; Generate new cached HTML file
267(define (update-html-file! html-file source-file #!optional force-update)
268  (when (or force-update
269            (not (file-exists? html-file))
270            (file-newer? source-file html-file))
271    (with-output-to-path html-file
272      (lambda ()
273        (let ((content (qwiki-sxml-page-template
274                        (call-with-input-file source-file wiki-parse))))
275          (output-xml content (qwiki-transformation-steps content)))))))
276
277;;; Request dispatching
278(define action-handlers
279  `((edit    . ,qwiki-edit)
280    (show    . ,qwiki-show)
281    (history . ,qwiki-history)))
282
283(define (read-request-data req)
284  (let ((len (header-value 'content-length (request-headers req))))
285    ;; If the header is not available, this will read until EOF
286    (read-string len (request-port req))))
287
288;; From Spiffy. Maybe export it there?
289(define (impossible-filename? name)
290  (or (string=? name ".") (string=? name "..") (string-index name #\/)))
291
292(define (ensure-latest-sources!)
293  (if (not (directory-exists? (qwiki-source-path)))
294      (checkout-sources! (qwiki-source-path))
295      ;; Not sure if this should be done every freaking time - it's slow!
296      #;(update-sources! (qwiki-source-path))
297      (void)))
298
299;; Spiffy handler for requests that should be routed to the wiki
300(define (qwiki-handler continue)
301  (ensure-latest-sources!)
302  (let ((uri (request-uri (current-request))))
303    (if (any impossible-filename? (cdr (uri-path uri))) ; assumed to be absolute
304        (begin
305          (read-request-data (current-request))
306          (send-status 404 "Not found"))
307        (let* ((action (string->symbol
308                        (alist-ref 'action (uri-query uri) eq? "show")))
309               (handler (alist-ref action action-handlers eq? qwiki-show)))
310          (handler (normalize-path (relative-uri-path uri))
311                   (current-request))))))
312
313(define (qwiki-render-file file)
314  (call-with-input-file file
315    (lambda (input)
316      (let ((content (qwiki-sxml-page-template (wiki-parse input))))
317        (output-xml content (qwiki-transformation-steps content))))))
318
319)
Note: See TracBrowser for help on using the repository browser.