Launchpad automatic translations update.
[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 from openerp import netsvc
39 from dav_fs import openerp_dav_handler
40 from openerp.tools.config import config
41 try:
42     from pywebdav.lib.WebDAVServer import DAVRequestHandler
43     from pywebdav.lib.utils import IfParser, TagList
44     from pywebdav.lib.errors import DAV_Error, DAV_Forbidden, DAV_NotFound
45     from pywebdav.lib.propfind import PROPFIND
46 except ImportError:
47     from DAV.WebDAVServer import DAVRequestHandler
48     from DAV.utils import IfParser, TagList
49     from DAV.errors import DAV_Error, DAV_Forbidden, DAV_NotFound
50     from DAV.propfind import PROPFIND
51 from openerp.service import http_server
52 from openerp.service.websrv_lib import FixSendError, HttpOptions
53 from BaseHTTPServer import BaseHTTPRequestHandler
54 import urlparse
55 import urllib
56 import re
57 import time
58 from string import atoi
59 import addons
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 = 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: