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