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