root/trunk/thirdparty/cl-webdav/resources.lisp

Revision 3342, 18.2 kB (checked in by edi, 7 months ago)

Update copyright for next release

  • Property svn:executable set to
Line 
1 ;;; -*- Mode: LISP; Syntax: COMMON-LISP; Package: CL-WEBDAV; Base: 10 -*-
2 ;;; $Header: /usr/local/cvsrep/cl-webdav/resources.lisp,v 1.12 2007/04/18 19:21:00 edi Exp $
3
4 ;;; Copyright (c) 2007-2008, Dr. Edmund Weitz.  All rights reserved.
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
16 ;;;     provided with the distribution.
17
18 ;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
19 ;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 ;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 ;;; ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
22 ;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 ;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
24 ;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25 ;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
26 ;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 ;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 ;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 (in-package :cl-webdav)
31
32 (defclass resource ()
33   ((script-name :initarg :script-name
34                 :accessor resource-script-name
35                 :documentation "This slot holds the script name
36 \(see HUNCHENTOOT:SCRIPT-NAME) that was used to create the
37 resource.  For objects you create yourself, you must provide a
38 meaningful value that can be used to access the resource."))
39   (:documentation "This is the base class you'll have to subclass
40 if you want to create your own custom DAV server.  Each object of
41 this class represents one resource on the server and most of the
42 time these objects are created by the server using only
43 the :SCRIPT-NAME initarg.  If you need more initialization to
44 happen, write an :AFTER method for INITIALIZE-INSTANCE.
45
46 See the file `file-resources.lisp' for an example of a subclass
47 of RESOURCE."))
48
49 ;;; The generic functions which /must/ be specialized
50
51 (defgeneric resource-exists (resource)
52   (:documentation "This function must return a true value if the
53 resource RESOURCE exists on the server and NIL otherwise.  You
54 must specialize this generic function for your own classes."))
55
56 (defgeneric resource-children (resource)
57   (:documentation "This function must return a list of all
58 children of RESOURCE \(which themselves are RESOURCE objects).
59 You must specialize this generic function for your own
60 classes."))
61
62 (defgeneric resource-parent (resource)
63   (:documentation "This function must return a RESOURCE object
64 which is the parent resource of RESOURCE or NIL if there is no
65 parent.  You must specialize this generic function for your own
66 classes."))
67
68 (defgeneric resource-collection-p (resource)
69   (:documentation "This function must return a true value if the
70 resource RESOURCE is a collection.  You must specialize this
71 generic function for your own classes."))
72
73 (defgeneric resource-write-date (resource)
74   (:documentation "This function must return a universal time
75 denoting the time the resource RESOURCE was last modified.  You
76 must specialize this generic function for your own classes."))
77
78 (defgeneric resource-length (resource)
79   (:documentation "This function must return an integer denoting
80 the length of the resource RESOURCE in octets.  You must
81 specialize this generic function for your own classes."))
82
83 (defgeneric resource-display-name (resource)
84   (:documentation "This function must return a string which,
85 according to the WebDAV RFC, \"provides a name for the resource
86 that is suitable for presentation to a user.\"  You must
87 specialize this generic function for your own classes."))
88
89 (defgeneric send-content (resource stream)
90   (:documentation "This function is called for GET requests and
91 must send the complete contents of the \(non-collection) resource
92 RESOURCE to the \(flexi) stream STREAM."))
93
94 (defgeneric get-content (resource stream length)
95   (:documentation "This function is called for PUT requests and
96 must read LENGTH octets of data from the \(flexi) stream STREAM
97 and store them in a place appropriate for the resource
98 RESOURCE."))
99
100 (defgeneric remove-resource (resource)
101   (:documentation "This function must completely remove the
102 resource RESOURCE.  It doesn't have to deal with dead properties,
103 and it can assume that RESOURCE doesn't have children in case
104 it's a collection."))
105
106 (defgeneric move-resource (source destination)
107   (:documentation "This function must \"move\" the \(contents of
108 the) resource SOURCE in such a way that it can in the future be
109 accessed as DESTINATION.  It doesn't have to deal with dead
110 properties, and it can assume that SOURCE doesn't have children
111 in case it's a collection."))
112
113 (defgeneric copy-resource (source destination)
114   (:documentation "This function must \"copy\" the \(contents of
115 the) resource SOURCE in such a way that the copy can in the
116 future be accessed as DESTINATION.  It doesn't have to deal with
117 dead properties, and it can assume that SOURCE doesn't have
118 children in case it's a collection."))
119
120 (defgeneric create-collection (resource)
121   (:documentation "This function must create a collection
122 resource that in the future can be accessed as RESOURCE."))
123
124 (defgeneric accept-request-p (resource-class request)
125   (:documentation "This must be a function which accepts a
126 Hunchentoot request object REQUEST and returns a generalized
127 boolean denoting whether REQUEST denotes a resource the DAV
128 server wants to handle.  Usually, you'll want to look at the
129 script name of the request or something like that - see the class
130 FILE-RESOURCE for an example.
131
132 Note that you specialize this function on the resource /class/
133 and not on the resource.")
134   (:method ((resource-class standard-class) script-name)
135    (accept-request-p (class-name resource-class) script-name)))
136
137 ;;; The generic functions which have default methods and thus don't
138 ;;; necessarily need to be specialized
139
140 (defgeneric resource-creation-date (resource)
141   (:documentation "This function must return a universal time
142 denoting the time the resource RESOURCE was created.  There's a
143 default method which returns RESOURCE-WRITE-DATE, but most likely
144 you'll want to specialize this for you own classes.")
145   (:method (resource)
146    (resource-write-date resource)))
147
148 (defgeneric resource-content-type (resource)
149   (:documentation "This function must return a string denoting
150 the MIME type of the resource RESOURCE.  It will only be called
151 if RESOURCE is /not/ a collection.  There's a default method
152 which always returns \"application/octet-stream\", but most
153 likely you'll want to specialize this for your own classes.")
154   (:method (resource) "application/octet-stream"))
155
156 (defgeneric resource-content-language (resource)
157   (:documentation "This function should return either NIL or a
158 language tag as defined in section 14.13 of RFC 2068.  If the
159 value returned by this function is not NIL, it will also be used
160 as the `Content-Language' header returned for GET requests.
161 There's a default method which always returns NIL.")
162   (:method (resource)))
163
164 (defgeneric resource-source (resource)
165   (:documentation "This function should return either NIL or a
166 DAV \"source\" XML node \(structured as an XMLS node) that,
167 according to the WebDAV RFC, \"identifies the resource that
168 contains the unprocessed source of the link's source.\" There's a
169 default method which always returns NIL.")
170   (:method (resource)))
171
172 (defgeneric resource-etag (resource)
173   (:documentation "This function should return an ETag for the
174 resource RESOURCE or NIL.  If the value returned by this function
175 is not NIL, it will also be used as the `ETag' header returned
176 for GET requests.  There's a default method which synthesizes a
177 value based on the script name and the write date of the
178 resource, and in most cases you probably don't need to specialize
179 this function.")
180   (:method (resource)
181    (md5-hex (format nil "~A-~A"
182                     (get-last-modified resource)
183                     (resource-script-name resource)))))
184
185 (defgeneric resource-type (resource)
186   (:documentation "This function should return either NIL or a
187 DAV \"resourcetype\" XML node \(structured as an XMLS node) that,
188 according to the WebDAV RFC, \"specifies the nature of the
189 resource.\"  There's a default method which returns something
190 fitting for collections and NIL otherwise, and in most cases you
191 probably don't need to specialize this function.")
192   (:method (resource)
193     (when (resource-collection-p resource)
194       (dav-node "resourcetype" (dav-node "collection")))))
195
196 (defgeneric resource-uri-prefix (resource)
197   (:documentation "This function must return a string which is
198 the part of a resource's HTTP or HTTPS URI that comprises the
199 scheme, the host, and the port and ends with a slash - something
200 like \"http://localhost:4242/\" or \"https://www.lisp.org/\".
201
202 The default method synthesizes this from the information
203 Hunchentoot provides and usually you only have to write your own
204 method if you're sitting behind a proxy.")
205   (:method (resource)
206     (format nil "http~:[~;s~]://~A~@[:~A~]/"
207             (ssl-p)
208             (ppcre:regex-replace ":\\d+$" (host) "")
209             (server-port))))
210
211 (defgeneric get-dead-properties (resource)
212   (:documentation "This function must return all dead properties
213 of the resource RESOURCE as a list of XML elements structured as
214 XMLS nodes.  There's a default method but you should definitely
215 specialize this for production servers.")
216   (:method (resource)
217    (retrieve-properties (resource-script-name resource))))
218
219 (defgeneric remove-dead-property (resource property)
220   (:documentation "This function must remove the currently stored
221 dead property designated by PROPERTY \(an XMLS node) of the
222 resource RESOURCE.  There's a default method but you should
223 definitely specialize this for production servers.")
224   (:method (resource property)
225    (store-properties (resource-script-name resource)
226                      (remove property (get-dead-properties resource)
227                              :test #'property-equal))))
228
229 (defgeneric set-dead-property (resource property)
230   (:documentation "This function must replace the currently
231 stored dead property designated by PROPERTY \(an XMLS node) of
232 the resource RESOURCE with PROPERTY, i.e. PROPERTY doubles as the
233 property itself and as the property designator.  There's a
234 default method but you should definitely specialize this for
235 production servers.")
236   (:method (resource property)
237    (store-properties (resource-script-name resource)
238                      (cons property
239                            (remove property (get-dead-properties resource)
240                                    :test #'property-equal)))))
241
242 (defgeneric remove-dead-properties (resource)
243   (:documentation "This function must remove all dead properties
244 of the resource RESOURCE.  There's a default method but you
245 should definitely specialize this for production servers.")
246   (:method (resource)
247    (remove-properties (resource-script-name resource))))
248
249 (defgeneric move-dead-properties (source destination)
250   (:documentation "This function must move all dead properties of
251 the resource SOURCE to the resource DESTINATION.  There's a
252 default method but you should definitely specialize this for
253 production servers.")
254   (:method (source destination)
255    (move-properties (resource-script-name source)
256                     (resource-script-name destination))))
257
258 (defgeneric copy-dead-properties (source destination)
259   (:documentation "This function must copy all dead properties of
260 the resource SOURCE to the resource DESTINATION.  There's a
261 default method but you should definitely specialize this for
262 production servers.")
263   (:method (source destination)
264    (copy-properties (resource-script-name source)
265                     (resource-script-name destination))))
266
267 ;;; Internal functionality
268
269 (defun resource-href (resource)
270   "Returns a URL-encoded version of the resource's script name for use
271 in HREF elements in property XML."
272   (format nil "~:[/~;~:*~{~A~^/~}~]"
273           (mapcar (lambda (string)
274                     (url-encode string +utf-8+))
275                   (ppcre:split "/" (resource-script-name resource)))))
276
277 (defun resource-name (resource)
278   "Retrieves and returns the \"name part\" of the script name of
279 RESOURCE, i.e. the last non-empty string behind a slash.  Note
280 that the result can be NIL.  This is a bit similar to
281 CL:FILE-NAMESTRING."
282   (first (last (ppcre:split "/" (resource-script-name resource)))))
283
284 (defun get-last-modified (resource)
285   "This is the function that is called for the
286 \"getlastmodified\" property.  It returns the result of
287 RESOURCE-WRITE-DATE as an RFC 1123 string within a DAV XML node."
288   (let ((node (dav-node "getlastmodified" (rfc-1123-date (resource-write-date resource)))))
289     (push '(("dt" . "urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/") "dateTime.rfc1123")
290           (node-attributes node))
291     node))
292
293 (defun creation-date (resource)
294   "This is the function that is called for the \"creationdate\"
295 property.  It returns the result of RESOURCE-CREATION-DATE as an
296 ISO 8601 string within a DAV XML node."
297   (let ((node (dav-node "creationdate" (iso-8601-date (resource-creation-date resource)))))
298     (push '(("dt" . "urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/") "dateTime.tz")
299           (node-attributes node))
300     node))
301
302 (defun get-content-length (resource)
303   "This is the function that is called for the
304 \"getcontentlength\" property.  It simply returns the result of
305 RESOURCE-LENGTH as a string."
306   (unless (resource-collection-p resource)
307     (format nil "~D" (resource-length resource))))
308
309 (defun get-content-type (resource)
310   "This is the function that is called for the \"getcontenttype\"
311 property.  It simply returns the result of RESOURCE-CONTENT-TYPE
312 for non-collections and \"httpd/unix-directory\" for
313 collections."
314   (cond ((resource-collection-p resource) "httpd/unix-directory")
315         (t (resource-content-type resource))))
316
317 (defun remove-resource* (resource)
318   "Removes the resource RESOURCE and \(if necessary) its children
319 using REMOVE-RESOURCE.  Returns a list of conses where the car is
320 an HTTP return code and the cdr is the corresponding resource for
321 exceptional situations encountered during the process."
322   (unless (resource-exists resource)
323     (return-from remove-resource*
324       (list (cons +http-not-found+ resource))))
325   (when (resource-collection-p resource)
326     ;; try to remove the children first
327     (let ((child-results
328             (loop for child in (resource-children resource)
329                   nconc (remove-resource* child))))
330       (when child-results
331         ;; stop recursion if something went wrong deeper down in the
332         ;; hierarchy
333         (return-from remove-resource* child-results))))
334   ;; remove the dead properties first
335   (remove-dead-properties resource)
336   (handler-case
337     (remove-resource resource)
338     (error (condition)
339       (warn "While trying to delete ~S: ~A"
340             (resource-script-name resource) condition)
341       (list (cons +http-internal-server-error+ resource)))
342     (:no-error (&rest args)
343       (declare (ignore args))
344       nil)))
345
346 (defun copy-or-move-resource* (source destination movep depth)
347   "Copies or moves \(depending on the generalized boolean MOVEP)
348 the resource denoted by SOURCE to \(the resource denoted by)
349 DESTINATION.  If DEPTH is NIL, recurses down to the children \(if
350 any) as well.  Returns a list of conses where the car is an HTTP
351 return code and the cdr is the corresponding \(source) resource
352 for exceptional situations encountered during the process."
353   (unless (resource-exists source)
354     (return-from copy-or-move-resource*
355       (list (cons +http-not-found+ source))))
356   ;; take care of dead properties
357   (funcall (if movep #'move-dead-properties #'copy-dead-properties)
358            source destination)
359   (let (results)
360     (handler-case
361         (funcall (if movep #'move-resource #'copy-resource)
362                  source destination)
363       (error (condition)
364         (warn "While trying to ~:[copy~;move~] from ~S to ~S: ~A"
365               movep
366               (resource-script-name source)
367               (resource-script-name destination)
368               condition)
369         (push (cons +http-internal-server-error+ source) results)))
370     (cond ((and (resource-collection-p source)
371                 (null depth)
372                 ;; only recurse if there weren't any errors
373                 (null results))
374            (loop for source-child in (resource-children source)
375                  for destination-child = (get-resource
376                                           ;; synthesize script name for new child
377                                           (format nil "~A~A"
378                                                       (resource-script-name destination)
379                                                       (resource-name source-child)))
380                  nconc (copy-or-move-resource* source-child destination-child depth movep)))
381           (t results))))
382
383 (defgeneric create-resource (resource-class script-name)
384   (:documentation "Creates and returns an object of type
385 RESOURCE-CLASS \(a subclass of RESOURCE) corresponding to the script
386 name SCRIPT-NAME \(which is already URL-decoded).")
387   (:method ((resource-class standard-class) script-name)
388    (create-resource (class-name resource-class) script-name))
389   (:method ((resource-class-name symbol) script-name)
390    (make-instance resource-class-name
391                   :script-name script-name)))
392
393 (defun get-resource (&optional (script-name (url-decode* (script-name*))))
394   "Creates and returns an object of the type stored in
395 *RESOURCE-CLASS* corresponding to the script name SCRIPT-NAME."
396   (create-resource *resource-class* script-name))
397
398 (defun resource-created (resource)
399   "Utility function which sets up Hunchentoot's *REPLY* object
400 for a +HTTP-CREATED+ response corresponding to the newly-created
401 resource RESOURCE."
402   (setf (content-type) (get-content-type resource)
403         (header-out :location) (resource-script-name resource)
404         (return-code) +http-created+)
405   (let ((etag (resource-etag resource))
406         (content-language (resource-content-language resource)))
407     (when etag
408       (setf (header-out :etag) etag))
409     (when content-language
410       (setf (header-out :content-language) content-language)))
411   nil)
Note: See TracBrowser for help on using the browser.