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

Last change on this file since 15439 was 15439, checked in by sjamaan, 11 years ago

Implement handling of conflicts

File size: 11.7 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
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;; The rules used for transforming page SXML structure
73(define (qwiki-transformation-steps content)
74  (append (qwiki-extensions)
75          ((qwiki-output-driver) content)
76          ))
77
78;; The basic template for SXML wiki pages
79(define (qwiki-sxml-page-template contents . headers)
80   `(wiki-page (Header ,@headers)
81               (body (page-specific-links)
82                     ,contents)))
83
84;; Return the trailing part of the path relative to the docroot/base-uri
85;; eg: If the wiki lives under /qwiki, /qwiki/eggref/4/9p gives /eggref/4/9p
86(define (relative-uri-path uri)
87  ;; Both URIs are assumed to contain absolute paths
88  (let loop ((path (cdr (uri-path uri)))
89             (base-path (cdr (uri-path (qwiki-base-uri)))))
90    (cond
91     ((or (null? base-path) (string-null? (car base-path))) path)
92     ((and (not (null? path))
93           (string=? (car path) (car base-path)))
94      (loop (cdr path) (cdr base-path)))
95     (else (error "Bad request URI path. Please configure qwiki-base-uri.")))))
96
97(define (path->html-filename path)
98  (make-pathname (qwiki-docroot)
99                 (string-join path "/") "html"))
100
101(define (path->source-filename path)
102  (make-pathname (qwiki-source-path) (string-join path "/")))
103
104;; Like with-output-to-file, only this creates parent directories as needed.
105(define (with-output-to-path path thunk)
106  (unless (file-exists? (pathname-directory path))
107    (create-directory (pathname-directory path) #t))
108  (with-output-to-file path thunk))
109
110(define (send-content content)
111  (write-logged-response)
112  (with-output-to-port (response-port (current-response))
113    (lambda ()
114      (output-xml content (qwiki-transformation-steps content))))
115  (close-output-port (response-port (current-response))))
116
117
118
119;;; Actions
120(define (qwiki-history path req)
121  (let* ((source-file (path->source-filename path))
122         (rev (string->number
123               (alist-ref 'rev (uri-query (request-uri req)) eq? "")))
124         (history (get-history source-file rev #f)) ; no pagination yet
125         (content (qwiki-sxml-page-template `(history ,history))))
126    (send-content content)))
127
128(define (qwiki-edit path req)
129  (let* ((html-file (path->html-filename path))
130         (source-file (path->source-filename path))
131         (postdata (if (eq? 'POST (request-method req))
132                       (form-urldecode (read-request-data req))
133                       '()))
134         (source (or (alist-ref 'source postdata)
135                     (and (file-exists? source-file) ;; XXX what if it's a dir?
136                          (with-input-from-file source-file read-string))
137                     ""))
138         (comment (alist-ref 'comment postdata eq? ""))
139         (username (alist-ref 'username postdata eq? ""))
140         (password (alist-ref 'password postdata eq? ""))
141         (auth (alist-ref 'auth postdata eq?))
142         ;; TODO: Clean this up, maybe put it in a transformation rule so
143         ;; it can be extended by plugins.  The names of the buttons are
144         ;; pretty much tied to the code though
145         (make-form
146          (lambda (#!optional message)
147            (qwiki-sxml-page-template 
148             `(,(if (alist-ref 'preview postdata)
149                    `(div (@ (class "preview"))
150                          (h2 "Preview")
151                          ,(wiki-parse source))
152                    "")
153               ,(if message
154                    `(div (@ class "message") ,message)
155                    "")
156               (p "username: " ,username " password: " ,password " auth: " ,auth)
157               (form (@ (method "post"))
158                     (label "Article contents:"
159                            (textarea (@ (name "source"))
160                                      ,source))
161                     (label "Description of your changes:"
162                            (textarea (@ (name "comment"))
163                                      ,comment))
164                     (label "I would like to authenticate"
165                            (input (@ (type "checkbox")
166                                      (name "auth")
167                                      ,@(if auth
168                                            '((checked "checked"))
169                                            '()))))
170                     (label "Username:"
171                            (input (@ (type "text")
172                                      (name "username")
173                                      (value ,username))))
174                     (label "Password:"
175                            (input (@ (type "password")
176                                      (name "password")
177                                      (value ,password))))
178                     (input (@ (type "submit")
179                               (name "save")
180                               (value "Save")))
181                     (input (@ (type "submit")
182                               (name "preview")
183                               (value "Preview")))))))))
184    (if (alist-ref 'save postdata)
185        (begin
186          (with-output-to-path source-file (lambda () (display source)))
187          (handle-exceptions exn
188            (begin
189              (undo-changes! source-file)
190              (update-sources! source-file)
191              (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."
192                                             (if auth
193                                                 " It is also possible your username/password are incorrect."
194                                                 "")))))
195            (store-changes! source-file comment
196                            (and auth username) (and auth password))
197            (redirect-to-qwiki-page req action: "show")))
198        (send-content (make-form)))))
199
200(define (redirect-to-qwiki-page req
201                                #!key
202                                ;; TODO: make path relative to qwiki-base-uri
203                                (path (uri-path (request-uri req)))
204                                (action "show"))
205  (with-headers `((location
206                   ,(update-uri (server-root-uri)
207                                path: path
208                                query: (alist-update!
209                                        'action action
210                                        (or (uri-query (request-uri req))
211                                            '())))))
212    ;; Maybe send a 303?
213    (lambda () (send-status 302 "Found"))))
214
215(define (qwiki-show path req)
216  ;; TODO: What if someone did something else than GET or HEAD?
217  (let* ((html-file (path->html-filename path))
218         (source-file (path->source-filename path))
219         (rev (string->number
220               (alist-ref 'rev (uri-query (request-uri req)) eq? ""))))
221    (if (file-exists? source-file)
222        (if rev
223            (send-content ; Do not store if old rev
224             (qwiki-sxml-page-template
225              (call-with-input-revision
226               source-file rev wiki-parse)))
227            (begin
228             (update-html-file! (make-pathname (root-path) html-file)
229                                source-file)
230             (send-static-file html-file)))
231        (redirect-to-qwiki-page req action: "edit"))))
232
233(define (file-newer? a b)
234  (> (file-modification-time a) (file-modification-time b)))
235
236;; Generate new cached HTML file
237(define (update-html-file! html-file source-file #!optional force-update)
238  (when (or force-update
239            (not (file-exists? html-file))
240            (file-newer? source-file html-file))
241    (with-output-to-path html-file
242      (lambda ()
243        (let ((content (qwiki-sxml-page-template
244                        (call-with-input-file source-file wiki-parse))))
245          (output-xml content (qwiki-transformation-steps content)))))))
246
247;;; Request dispatching
248(define action-handlers
249  `((edit    . ,qwiki-edit)
250    (show    . ,qwiki-show)
251    (history . ,qwiki-history)))
252
253(define (read-request-data req)
254  (let ((len (header-value 'content-length (request-headers req))))
255    ;; If the header is not available, this will read until EOF
256    (read-string len (request-port req))))
257
258;; From Spiffy. Maybe export it there?
259(define (impossible-filename? name)
260  (or (string=? name ".") (string=? name "..") (string-index name #\/)))
261
262(define (ensure-latest-sources!)
263  (if (not (directory-exists? (qwiki-source-path)))
264      (checkout-sources! (qwiki-source-path))
265      ;; Not sure if this should be done every freaking time - it's slow!
266      #;(update-sources! (qwiki-source-path))
267      (void)))
268
269;; Spiffy handler for requests that should be routed to the wiki
270(define (qwiki-handler continue)
271  (ensure-latest-sources!)
272  (let ((uri (request-uri (current-request))))
273    (if (any impossible-filename? (cdr (uri-path uri))) ; assumed to be absolute
274        (begin
275          (read-request-data (current-request))
276          (send-status 404 "Not found"))
277        (let* ((action (string->symbol
278                        (alist-ref 'action (uri-query uri) eq? "show")))
279               (handler (alist-ref action action-handlers eq? qwiki-show)))
280          (handler (relative-uri-path uri) (current-request))))))
281
282(define (qwiki-render-file file)
283  (call-with-input-file file
284    (lambda (input)
285      (let ((content (qwiki-sxml-page-template (wiki-parse input))))
286        (output-xml content (qwiki-transformation-steps content))))))
287
288)
Note: See TracBrowser for help on using the repository browser.