Corrected report view
[odoo/odoo.git] / addons / document_webdav / webdav_server.py
1 # -*- encoding: utf-8 -*-
2 ############################################################################9
3 #
4 # Copyright P. Christeas <p_christ@hol.gr> 2008-2010
5 # Copyright OpenERP SA, 2010 (http://www.openerp.com )
6 #
7 # Disclaimer: Many of the functions below borrow code from the
8 #   python-webdav library (http://code.google.com/p/pywebdav/ ),
9 #   which they import and override to suit OpenERP functionality.
10 # python-webdav was written by: Simon Pamies <s.pamies@banality.de>
11 #                               Christian Scholz <mrtopf@webdav.de>
12 #                               Vince Spicer <vince@vince.ca>
13 #
14 # WARNING: This program as such is intended to be used by professional
15 # programmers who take the whole responsability of assessing all potential
16 # consequences resulting from its eventual inadequacies and bugs
17 # End users who are looking for a ready-to-use solution with commercial
18 # garantees and support are strongly adviced to contract a Free Software
19 # Service Company
20 #
21 # This program is Free Software; you can redistribute it and/or
22 # modify it under the terms of the GNU General Public License
23 # as published by the Free Software Foundation; either version 3
24 # of the License, or (at your option) any later version.
25 #
26 # This program is distributed in the hope that it will be useful,
27 # but WITHOUT ANY WARRANTY; without even the implied warranty of
28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
29 # GNU General Public License for more details.
30 #
31 # You should have received a copy of the GNU General Public License
32 # along with this program; if not, write to the Free Software
33 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
34 ###############################################################################
35
36
37 import logging
38 import openerp
39 from openerp import netsvc
40 from dav_fs import openerp_dav_handler
41 from openerp.tools.config import config
42 try:
43     from pywebdav.lib.WebDAVServer import DAVRequestHandler
44     from pywebdav.lib.utils import IfParser, TagList
45     from pywebdav.lib.errors import DAV_Error, DAV_Forbidden, DAV_NotFound
46     from pywebdav.lib.propfind import PROPFIND
47 except ImportError:
48     from DAV.WebDAVServer import DAVRequestHandler
49     from DAV.utils import IfParser, TagList
50     from DAV.errors import DAV_Error, DAV_Forbidden, DAV_NotFound
51     from DAV.propfind import PROPFIND
52 from openerp.service import http_server
53 from openerp.service.websrv_lib import FixSendError, HttpOptions
54 from BaseHTTPServer import BaseHTTPRequestHandler
55 import urlparse
56 import urllib
57 import re
58 import time
59 from string import atoi
60 import socket
61 # from DAV.constants import DAV_VERSION_1, DAV_VERSION_2
62 from xml.dom import minidom
63 from redirect import RedirectHTTPHandler
64 _logger = logging.getLogger(__name__)
65 khtml_re = re.compile(r' KHTML/([0-9\.]+) ')
66
67 def OpenDAVConfig(**kw):
68     class OpenDAV:
69         def __init__(self, **kw):
70             self.__dict__.update(**kw)
71
72         def getboolean(self, word):
73             return self.__dict__.get(word, False)
74
75     class Config:
76         DAV = OpenDAV(**kw)
77
78     return Config()
79
80
81 class DAVHandler(DAVRequestHandler, HttpOptions, FixSendError):
82     verbose = False
83
84     protocol_version = 'HTTP/1.1'
85     _HTTP_OPTIONS= { 'DAV' : ['1', '2'],
86                     'Allow' : [ 'GET', 'HEAD', 'COPY', 'MOVE', 'POST', 'PUT',
87                             'PROPFIND', 'PROPPATCH', 'OPTIONS', 'MKCOL',
88                             'DELETE', 'TRACE', 'REPORT', ]
89                     }
90
91     def __init__(self, request, client_address, server):
92         self.request = request
93         self.client_address = client_address
94         self.server = server
95         self.setup()
96
97     def get_userinfo(self, user, pw):
98         return False
99
100     def _log(self, message):
101         self._logger.debug(message)
102
103     def handle(self):
104         """Handle multiple requests if necessary."""
105         self.close_connection = 1
106         try:
107             self.handle_one_request()
108             while not self.close_connection:
109                 self.handle_one_request()
110         except Exception as e:
111             try:
112                 self.log_error("Request timed out: %r \n Trying old version of HTTPServer", e)
113                 self._init_buffer()
114             except Exception as e:
115                 #a read or a write timed out.  Discard this connection
116                 self.log_error("Not working neither, closing connection\n %r", e)
117                 self.close_connection = 1
118
119     def finish(self):
120         pass
121
122     def get_db_from_path(self, uri):
123         # interface class will handle all cases.
124         res =  self.IFACE_CLASS.get_db(uri, allow_last=True)
125         return res
126
127     def setup(self):
128         self.davpath = '/'+config.get_misc('webdav','vdir','webdav')
129         addr, port = self.server.server_name, self.server.server_port
130         server_proto = getattr(self.server,'proto', 'http').lower()
131         # Too early here to use self.headers
132         self.baseuri = "%s://%s:%d/"% (server_proto, addr, port)
133         self.IFACE_CLASS  = openerp_dav_handler(self, self.verbose)
134
135     def copymove(self, CLASS):
136         """ Our uri scheme removes the /webdav/ component from there, so we
137         need to mangle the header, too.
138         """
139         up = urlparse.urlparse(urllib.unquote(self.headers['Destination']))
140         if up.path.startswith(self.davpath):
141             self.headers['Destination'] = up.path[len(self.davpath):]
142         else:
143             raise DAV_Forbidden("Not allowed to copy/move outside webdav path.")
144         # TODO: locks
145         DAVRequestHandler.copymove(self, CLASS)
146
147     def get_davpath(self):
148         return self.davpath
149
150     def log_message(self, format, *args):
151         _logger.debug(format % args)
152
153     def log_error(self, format, *args):
154         _logger.warning(format % args)
155
156     def _prep_OPTIONS(self, opts):
157         ret = opts
158         dc=self.IFACE_CLASS
159         uri=urlparse.urljoin(self.get_baseuri(dc), self.path)
160         uri=urllib.unquote(uri)
161         try:
162             ret = dc.prep_http_options(uri, opts)
163         except DAV_Error, (ec,dd):
164             pass
165         except Exception,e:
166             self.log_error("Error at options: %s", str(e))
167             raise
168         return ret
169
170     def send_response(self, code, message=None):
171         # the BufferingHttpServer will send Connection: close , while
172         # the BaseHTTPRequestHandler will only accept int code.
173         # workaround both of them.
174         if self.command == 'PROPFIND' and int(code) == 404:
175             kh = khtml_re.search(self.headers.get('User-Agent',''))
176             if kh and (kh.group(1) < '4.5'):
177                 # There is an ugly bug in all khtml < 4.5.x, where the 404
178                 # response is treated as an immediate error, which would even
179                 # break the flow of a subsequent PUT request. At the same time,
180                 # the 200 response  (rather than 207 with content) is treated
181                 # as "path not exist", so we send this instead
182                 # https://bugs.kde.org/show_bug.cgi?id=166081
183                 code = 200
184         BaseHTTPRequestHandler.send_response(self, int(code), message)
185
186     def send_header(self, key, value):
187         if key == 'Connection' and value == 'close':
188             self.close_connection = 1
189         DAVRequestHandler.send_header(self, key, value)
190
191     def send_body(self, DATA, code=None, msg=None, desc=None, ctype='application/octet-stream', headers=None):
192         if headers and 'Connection' in headers:
193             pass
194         elif self.request_version in ('HTTP/1.0', 'HTTP/0.9'):
195             pass
196         elif self.close_connection == 1: # close header already sent
197             pass
198         elif headers and self.headers.get('Connection',False) == 'Keep-Alive':
199             headers['Connection'] = 'keep-alive'
200
201         if headers is None:
202             headers = {}
203
204         DAVRequestHandler.send_body(self, DATA, code=code, msg=msg, desc=desc,
205                     ctype=ctype, headers=headers)
206
207     def do_PUT(self):
208         dc=self.IFACE_CLASS
209         uri=urlparse.urljoin(self.get_baseuri(dc), self.path)
210         uri=urllib.unquote(uri)
211         # Handle If-Match
212         if self.headers.has_key('If-Match'):
213             test = False
214             etag = None
215
216             for match in self.headers['If-Match'].split(','):
217                 if match == '*':
218                     if dc.exists(uri):
219                         test = True
220                         break
221                 else:
222                     if dc.match_prop(uri, match, "DAV:", "getetag"):
223                         test = True
224                         break
225             if not test:
226                 self._get_body()
227                 self.send_status(412)
228                 return
229
230         # Handle If-None-Match
231         if self.headers.has_key('If-None-Match'):
232             test = True
233             etag = None
234             for match in self.headers['If-None-Match'].split(','):
235                 if match == '*':
236                     if dc.exists(uri):
237                         test = False
238                         break
239                 else:
240                     if dc.match_prop(uri, match, "DAV:", "getetag"):
241                         test = False
242                         break
243             if not test:
244                 self._get_body()
245                 self.send_status(412)
246                 return
247
248         # Handle expect
249         expect = self.headers.get('Expect', '')
250         if (expect.lower() == '100-continue' and
251                 self.protocol_version >= 'HTTP/1.1' and
252                 self.request_version >= 'HTTP/1.1'):
253             self.send_status(100)
254             self._flush()
255
256         # read the body
257         body=self._get_body()
258
259         # locked resources are not allowed to be overwritten
260         if self._l_isLocked(uri):
261             return self.send_body(None, '423', 'Locked', 'Locked')
262
263         ct=None
264         if self.headers.has_key("Content-Type"):
265             ct=self.headers['Content-Type']
266         try:
267             location = dc.put(uri, body, ct)
268         except DAV_Error, (ec,dd):
269             self.log_error("Cannot PUT to %s: %s", uri, dd)
270             return self.send_status(ec)
271
272         headers = {}
273         etag = None
274         if location and isinstance(location, tuple):
275             etag = location[1]
276             location = location[0]
277             # note that we have allowed for > 2 elems
278         if location:
279             headers['Location'] = location
280         else:
281             try:
282                 if not etag:
283                     etag = dc.get_prop(location or uri, "DAV:", "getetag")
284                 if etag:
285                     headers['ETag'] = str(etag)
286             except Exception:
287                 pass
288
289         self.send_body(None, '201', 'Created', '', headers=headers)
290
291     def _get_body(self):
292         body = None
293         if self.headers.has_key("Content-Length"):
294             l=self.headers['Content-Length']
295             body=self.rfile.read(atoi(l))
296         return body
297
298     def do_DELETE(self):
299         try:
300             DAVRequestHandler.do_DELETE(self)
301         except DAV_Error, (ec, dd):
302             return self.send_status(ec)
303
304     def do_UNLOCK(self):
305         """ Unlocks given resource """
306
307         dc = self.IFACE_CLASS
308         self.log_message('UNLOCKing resource %s' % self.headers)
309
310         uri = urlparse.urljoin(self.get_baseuri(dc), self.path)
311         uri = urllib.unquote(uri)
312
313         token = self.headers.get('Lock-Token', False)
314         if token:
315             token = token.strip()
316             if token[0] == '<' and token[-1] == '>':
317                 token = token[1:-1]
318             else:
319                 token = False
320
321         if not token:
322             return self.send_status(400, 'Bad lock token')
323
324         try:
325             res = dc.unlock(uri, token)
326         except DAV_Error, (ec, dd):
327             return self.send_status(ec, dd)
328
329         if res == True:
330             self.send_body(None, '204', 'OK', 'Resource unlocked.')
331         else:
332             # We just differentiate the description, for debugging purposes
333             self.send_body(None, '204', 'OK', 'Resource not locked.')
334
335     def do_LOCK(self):
336         """ Attempt to place a lock on the given resource.
337         """
338
339         dc = self.IFACE_CLASS
340         lock_data = {}
341
342         self.log_message('LOCKing resource %s' % self.headers)
343
344         body = None
345         if self.headers.has_key('Content-Length'):
346             l = self.headers['Content-Length']
347             body = self.rfile.read(atoi(l))
348
349         depth = self.headers.get('Depth', 'infinity')
350
351         uri = urlparse.urljoin(self.get_baseuri(dc), self.path)
352         uri = urllib.unquote(uri)
353         self.log_message('do_LOCK: uri = %s' % uri)
354
355         ifheader = self.headers.get('If')
356
357         if ifheader:
358             ldif = IfParser(ifheader)
359             if isinstance(ldif, list):
360                 if len(ldif) !=1 or (not isinstance(ldif[0], TagList)) \
361                         or len(ldif[0].list) != 1:
362                     raise DAV_Error(400, "Cannot accept multiple tokens.")
363                 ldif = ldif[0].list[0]
364                 if ldif[0] == '<' and ldif[-1] == '>':
365                     ldif = ldif[1:-1]
366
367             lock_data['token'] = ldif
368
369         if not body:
370             lock_data['refresh'] = True
371         else:
372             lock_data['refresh'] = False
373             lock_data.update(self._lock_unlock_parse(body))
374
375         if lock_data['refresh'] and not lock_data.get('token', False):
376             raise DAV_Error(400, 'Lock refresh must specify token.')
377
378         lock_data['depth'] = depth
379
380         try:
381             created, data, lock_token = dc.lock(uri, lock_data)
382         except DAV_Error, (ec, dd):
383             return self.send_status(ec, dd)
384
385         headers = {}
386         if not lock_data['refresh']:
387             headers['Lock-Token'] = '<%s>' % lock_token
388
389         if created:
390             self.send_body(data, '201', 'Created',  ctype='text/xml', headers=headers)
391         else:
392             self.send_body(data, '200', 'OK', ctype='text/xml', headers=headers)
393
394     def _lock_unlock_parse(self, body):
395         # Override the python-webdav function, with some improvements
396         # Unlike the py-webdav one, we also parse the owner minidom elements into
397         # pure pythonic struct.
398         doc = minidom.parseString(body)
399
400         data = {}
401         owners = []
402         for info in doc.getElementsByTagNameNS('DAV:', 'lockinfo'):
403             for scope in info.getElementsByTagNameNS('DAV:', 'lockscope'):
404                 for scc in scope.childNodes:
405                     if scc.nodeType == info.ELEMENT_NODE \
406                             and scc.namespaceURI == 'DAV:':
407                         data['lockscope'] = scc.localName
408                         break
409             for ltype in info.getElementsByTagNameNS('DAV:', 'locktype'):
410                 for ltc in ltype.childNodes:
411                     if ltc.nodeType == info.ELEMENT_NODE \
412                             and ltc.namespaceURI == 'DAV:':
413                         data['locktype'] = ltc.localName
414                         break
415             for own in info.getElementsByTagNameNS('DAV:', 'owner'):
416                 for ono in own.childNodes:
417                     if ono.nodeType == info.TEXT_NODE:
418                         if ono.data:
419                             owners.append(ono.data)
420                     elif ono.nodeType == info.ELEMENT_NODE \
421                             and ono.namespaceURI == 'DAV:' \
422                             and ono.localName == 'href':
423                         href = ''
424                         for hno in ono.childNodes:
425                             if hno.nodeType == info.TEXT_NODE:
426                                 href += hno.data
427                         owners.append(('href','DAV:', href))
428
429             if len(owners) == 1:
430                 data['lockowner'] = owners[0]
431             elif not owners:
432                 pass
433             else:
434                 data['lockowner'] = owners
435         return data
436
437 from openerp.service.http_server import reg_http_service,OpenERPAuthProvider
438
439 class DAVAuthProvider(OpenERPAuthProvider):
440     def authenticate(self, db, user, passwd, client_address):
441         """ authenticate, but also allow the False db, meaning to skip
442             authentication when no db is specified.
443         """
444         if db is False:
445             return True
446         return OpenERPAuthProvider.authenticate(self, db, user, passwd, client_address)
447
448
449 class dummy_dav_interface(object):
450     """ Dummy dav interface """
451     verbose = True
452
453     PROPS={"DAV:" : ('creationdate',
454                      'displayname',
455                      'getlastmodified',
456                      'resourcetype',
457                      ),
458            }
459
460     M_NS={"DAV:" : "_get_dav", }
461
462     def __init__(self, parent):
463         self.parent = parent
464
465     def get_propnames(self, uri):
466         return self.PROPS
467
468     def get_prop(self, uri, ns, propname):
469         if self.M_NS.has_key(ns):
470             prefix=self.M_NS[ns]
471         else:
472             raise DAV_NotFound
473         mname=prefix+"_"+propname.replace('-', '_')
474         try:
475             m=getattr(self,mname)
476             r=m(uri)
477             return r
478         except AttributeError:
479             raise DAV_NotFound
480
481     def get_data(self, uri, range=None):
482         raise DAV_NotFound
483
484     def _get_dav_creationdate(self, uri):
485         return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
486
487     def _get_dav_getlastmodified(self, uri):
488         return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
489
490     def _get_dav_displayname(self, uri):
491         return uri
492
493     def _get_dav_resourcetype(self, uri):
494         return ('collection', 'DAV:')
495
496     def exists(self, uri):
497         """ return 1 or None depending on if a resource exists """
498         uri2 = uri.split('/')
499         if len(uri2) < 3:
500             return True
501         _logger.debug("Requested uri: %s", uri)
502         return None # no
503
504     def is_collection(self, uri):
505         """ return 1 or None depending on if a resource is a collection """
506         return None # no
507
508 class DAVStaticHandler(http_server.StaticHTTPHandler):
509     """ A variant of the Static handler, which will serve dummy DAV requests
510     """
511
512     verbose = False
513     protocol_version = 'HTTP/1.1'
514     _HTTP_OPTIONS= { 'DAV' : ['1', '2'],
515                     'Allow' : [ 'GET', 'HEAD',
516                             'PROPFIND', 'OPTIONS', 'REPORT', ]
517                     }
518
519     def send_body(self, content, code, message='OK', content_type='text/xml'):
520         self.send_response(int(code), message)
521         self.send_header("Content-Type", content_type)
522         # self.send_header('Connection', 'close')
523         self.send_header('Content-Length', len(content) or 0)
524         self.end_headers()
525         if hasattr(self, '_flush'):
526             self._flush()
527
528         if self.command != 'HEAD':
529             self.wfile.write(content)
530
531     def do_PROPFIND(self):
532         """Answer to PROPFIND with generic data.
533
534         A rough copy of python-webdav's do_PROPFIND, but hacked to work
535         statically.
536         """
537
538         dc = dummy_dav_interface(self)
539
540         # read the body containing the xml request
541         # iff there is no body then this is an ALLPROP request
542         body = None
543         if self.headers.has_key('Content-Length'):
544             l = self.headers['Content-Length']
545             body = self.rfile.read(atoi(l))
546
547         path = self.path.rstrip('/')
548         uri = urllib.unquote(path)
549
550         pf = PROPFIND(uri, dc, self.headers.get('Depth', 'infinity'), body)
551
552         try:
553             DATA = '%s\n' % pf.createResponse()
554         except DAV_Error, (ec,dd):
555             return self.send_error(ec,dd)
556         except Exception:
557             self.log_exception("Cannot PROPFIND")
558             raise
559
560         # work around MSIE DAV bug for creation and modified date
561         # taken from Resource.py @ Zope webdav
562         if (self.headers.get('User-Agent') ==
563             'Microsoft Data Access Internet Publishing Provider DAV 1.1'):
564             DATA = DATA.replace('<ns0:getlastmodified xmlns:ns0="DAV:">',
565                                     '<ns0:getlastmodified xmlns:n="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/" b:dt="dateTime.rfc1123">')
566             DATA = DATA.replace('<ns0:creationdate xmlns:ns0="DAV:">',
567                                     '<ns0:creationdate xmlns:n="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/" b:dt="dateTime.tz">')
568
569         self.send_body(DATA, '207','Multi-Status','Multiple responses')
570
571     def not_get_baseuri(self):
572         baseuri = '/'
573         if self.headers.has_key('Host'):
574             uparts = list(urlparse.urlparse('/'))
575             uparts[1] = self.headers['Host']
576             baseuri = urlparse.urlunparse(uparts)
577         return baseuri
578
579     def get_davpath(self):
580         return ''
581
582
583 try:
584
585     if (config.get_misc('webdav','enable',True)):
586         directory = '/'+config.get_misc('webdav','vdir','webdav')
587         handler = DAVHandler
588         verbose = config.get_misc('webdav','verbose',True)
589         handler.debug = config.get_misc('webdav','debug',True)
590         _dc = { 'verbose' : verbose,
591                 'directory' : directory,
592                 'lockemulation' : True,
593                 }
594
595         conf = OpenDAVConfig(**_dc)
596         handler._config = conf
597         reg_http_service(directory, DAVHandler, DAVAuthProvider)
598         _logger.info("WebDAV service registered at path: %s/ "% directory)
599
600         if not (config.get_misc('webdav', 'no_root_hack', False)):
601             # Now, replace the static http handler with the dav-enabled one.
602             # If a static-http service has been specified for our server, then
603             # read its configuration and use that dir_path.
604             # NOTE: this will _break_ any other service that would be registered
605             # at the root path in future.
606             base_path = False
607             if config.get_misc('static-http','enable', False):
608                 base_path = config.get_misc('static-http', 'base_path', '/')
609             if base_path and base_path == '/':
610                 dir_path = config.get_misc('static-http', 'dir_path', False)
611             else:
612                 dir_path = openerp.addons.get_module_resource('document_webdav','public_html')
613                 # an _ugly_ hack: we put that dir back in tools.config.misc, so that
614                 # the StaticHttpHandler can find its dir_path.
615                 config.misc.setdefault('static-http',{})['dir_path'] = dir_path
616
617             reg_http_service('/', DAVStaticHandler)
618
619 except Exception, e:
620     _logger.error('Cannot launch webdav: %s' % e)
621
622
623 def init_well_known():
624     reps = RedirectHTTPHandler.redirect_paths
625
626     num_svcs = config.get_misc('http-well-known', 'num_services', '0')
627
628     for nsv in range(1, int(num_svcs)+1):
629         uri = config.get_misc('http-well-known', 'service_%d' % nsv, False)
630         path = config.get_misc('http-well-known', 'path_%d' % nsv, False)
631         if not (uri and path):
632             continue
633         reps['/'+uri] = path
634
635     if int(num_svcs):
636         reg_http_service('/.well-known', RedirectHTTPHandler)
637
638 init_well_known()
639
640 class PrincipalsRedirect(RedirectHTTPHandler):
641
642
643     redirect_paths = {}
644
645     def _find_redirect(self):
646         for b, r in self.redirect_paths.items():
647             if self.path.startswith(b):
648                 return r + self.path[len(b):]
649         return False
650
651 def init_principals_redirect():
652     """ Some devices like the iPhone will look under /principals/users/xxx for
653     the user's properties. In OpenERP we _cannot_ have a stray /principals/...
654     working path, since we have a database path and the /webdav/ component. So,
655     the best solution is to redirect the url with 301. Luckily, it does work in
656     the device. The trick is that we need to hard-code the database to use, either
657     the one centrally defined in the config, or a "forced" one in the webdav
658     section.
659     """
660     dbname = config.get_misc('webdav', 'principal_dbname', False)
661     if (not dbname) and not config.get_misc('webdav', 'no_principals_redirect', False):
662         dbname = config.get('db_name', False)
663     if dbname:
664         PrincipalsRedirect.redirect_paths[''] = '/webdav/%s/principals' % dbname
665         reg_http_service('/principals', PrincipalsRedirect)
666         _logger.info(
667                 "Registered HTTP redirect handler for /principals to the %s db.",
668                 dbname)
669
670 init_principals_redirect()
671
672 #eof
673
674
675
676
677 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: