Commit 88f4bed7 authored by 's avatar

cleanup

parent 5edd9bec
......@@ -85,10 +85,11 @@
"""WebDAV support - collection objects."""
__version__='$Revision: 1.1 $'[11:-2]
__version__='$Revision: 1.2 $'[11:-2]
import sys, os, string, mimetypes
import sys, os, string
from Resource import Resource
from common import urlfix
class Collection(Resource):
......@@ -96,8 +97,9 @@ class Collection(Resource):
collection objects. It provides default implementations
for all supported WebDAV HTTP methods. The behaviors of some
WebDAV HTTP methods for collections are slightly different
than those for non-collection resources.
"""
than those for non-collection resources."""
__dav_collection__=1
def redirect_check(self, req, rsp):
# By the spec, we are not supposed to accept /foo for a
......@@ -126,14 +128,13 @@ class Collection(Resource):
"""Delete a collection resource. For collection resources, DELETE
may return either 200 (OK) or 204 (No Content) to indicate total
success, or may return 207 (Multistatus) to indicate partial
success."""
success. Note that in Zope a DELETE never returns 207."""
self.init_headers(RESPONSE)
self.redirect_check(REQUEST, RESPONSE)
if self.dav__is_acquired():
raise 'Not Found', 'The requested resource does not exist.'
path=filter(None, string.split(REQUEST['PATH_INFO'], '/'))
name=path[-1]
# TODO: add lock check here
url=urlfix(REQUEST['URL'], 'DELETE')
name=filter(None, string.split(url, '/'))[-1]
# TODO: add lock checking here
self.aq_parent._delObject(name)
RESPONSE.setStatus(204)
return RESPONSE
......@@ -85,22 +85,25 @@
"""WebDAV support - null resource objects."""
__version__='$Revision: 1.1 $'[11:-2]
__version__='$Revision: 1.2 $'[11:-2]
import sys, os, string, mimetypes
import Acquisition, OFS.content_types
from Resource import Resource, aq_base
from common import absattr, aq_base, urlfix
from Resource import Resource
from Globals import Persistent
class NullResource(Acquisition.Implicit, Resource):
class NullResource(Persistent, Acquisition.Implicit, Resource):
"""Null resources are used to handle HTTP method calls on
objects which do not yet exist in the url namespace."""
_isNullResource=1
__dav_null__=1
def __init__(self, parent, id):
self.id=id
self.__parent__=parent
self.__roles__=None # fix this!!
self.__roles__=parent.__roles__
def HEAD(self, REQUEST, RESPONSE):
"""Retrieve resource information without a response message body."""
......@@ -143,8 +146,7 @@ class NullResource(Acquisition.Implicit, Resource):
parent=self.__parent__
if hasattr(aq_base(parent), self.id):
raise 'Method Not Allowed', 'The name %s is in use.' % self.id
if (not hasattr(parent.aq_base, 'isAnObjectManager')) or \
(not parent.isAnObjectManager):
if not hasattr(parent, '__dav_collection__'):
raise 'Forbidden', 'Unable to create collection resource.'
# This should probably do self.__class__(id, ...), except Folder
# doesn't currently have a constructor.
......@@ -161,5 +163,4 @@ class NullResource(Acquisition.Implicit, Resource):
def UNLOCK(self):
"""Remove a lock-null resource."""
self.init_headers(RESPONSE)
raise 'Method Not Allowed', 'Method not supported for this resource.'
......@@ -85,13 +85,12 @@
"""WebDAV support - resource objects."""
__version__='$Revision: 1.1 $'[11:-2]
__version__='$Revision: 1.2 $'[11:-2]
import sys, os, string, time
import mimetypes, xmlcmds
zpns='http://www.zope.org/propertysets/default/'
from common import absattr, aq_base
from common import urlfix, rfc1123_date
class Resource:
......@@ -101,13 +100,12 @@ class Resource:
such as PUT should be overridden to ensure correct behavior in
the context of the object type."""
__dav_resource__=1
__http_methods__=('GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS',
'TRACE', 'PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY',
'MOVE',
)
__dav_resource__=1
def init_headers(self, r):
# Init expected HTTP 1.1 / WebDAV headers which are not
# currently set by the response object automagically.
......@@ -130,35 +128,16 @@ class Resource:
lock=Lock('xxxx', 'xxxx')
return self.dav__locks + (lock,)
def dav__is_acquired(self, ob=None):
# Return true if this object is not a direct
# subobject of its aq_parent object.
if ob is None: ob=self
if not hasattr(ob, 'aq_parent'):
return 0
if hasattr(aq_base(ob.aq_parent), absattr(ob.id)):
return 0
if hasattr(aq_base(ob), 'isTopLevelPrincipiaApplicationObject'):
return 0
return 1
# WebDAV class 1 support
def HEAD(self, REQUEST, RESPONSE):
"""Retrieve resource information without a response message
body. It would be great if we had a standard way to ask an
arbitrary object for its headers -- that would allow the
default HEAD implementation to handle most needs."""
"""Retrieve resource information without a response body."""
self.init_headers(RESPONSE)
raise 'Method Not Allowed', 'Method not supported for this resource.'
def PUT(self, REQUEST, RESPONSE):
"""Replace the GET response entity of an existing resource.
Because this is often object-dependent, objects which handle
PUT should override the default PUT implementation with an
object-specific implementation. By default, PUT requests
......@@ -176,7 +155,7 @@ class Resource:
def TRACE(self, REQUEST, RESPONSE):
"""Return the HTTP message received back to the client as the
entity-body of a 200 (OK) response. This will often actually
entity-body of a 200 (OK) response. This will often usually
be intercepted by the web server in use. If not, the TRACE
request will fail with a 405 (Method Not Allowed), since it
is not often possible to reproduce the HTTP request verbatim
......@@ -188,12 +167,8 @@ class Resource:
"""Delete a resource. For non-collection resources, DELETE may
return either 200 or 204 (No Content) to indicate success."""
self.init_headers(RESPONSE)
if self.dav__is_acquired():
raise 'Not Found', 'The requested resource does not exist.'
path=filter(None, string.split(REQUEST['URL'], '/'))
if path[-1]=='DELETE':
del path[-1]
name=path[-1]
url=urlfix(REQUEST['URL'], 'DELETE')
name=filter(None, string.split(url, '/'))[-1]
# TODO: add lock checking here
self.aq_parent._delObject(name)
RESPONSE.setStatus(204)
......@@ -202,11 +177,9 @@ class Resource:
def PROPFIND(self, REQUEST, RESPONSE):
"""Retrieve properties defined on the resource."""
self.init_headers(RESPONSE)
if self.dav__is_acquired():
raise 'Not Found', 'The requested resource does not exist.'
try: request=xmlcmds.PropFind(REQUEST)
try: cmd=xmlcmds.PropFind(REQUEST)
except: raise 'Bad Request', 'Invalid xml request.'
result=request.apply(self)
result=cmd.apply(self)
RESPONSE.setStatus(207)
RESPONSE.setHeader('Content-Type', 'text/xml; charset="utf-8"')
RESPONSE.setBody(result)
......@@ -215,15 +188,13 @@ class Resource:
def PROPPATCH(self, REQUEST, RESPONSE):
"""Set and/or remove properties defined on the resource."""
self.init_headers(RESPONSE)
if self.dav__is_acquired():
raise 'Not Found', 'The requested resource does not exist.'
if not hasattr(self, '__propsets__'):
raise 'Method Not Allowed', (
'Method not supported for this resource.')
# TODO: add lock checking here
try: request=xmlcmds.PropPatch(REQUEST)
try: cmd=xmlcmds.PropPatch(REQUEST)
except: raise 'Bad Request', 'Invalid xml request.'
result=request.apply(self)
result=cmd.apply(self)
RESPONSE.setStatus(207)
RESPONSE.setHeader('Content-Type', 'text/xml; charset="utf-8"')
RESPONSE.setBody(result)
......@@ -245,8 +216,6 @@ class Resource:
if not hasattr(aq_base(self), 'cb_isCopyable') or \
not self.cb_isCopyable():
raise 'Method Not Allowed', 'This object may not be copied.'
if self.dav__is_acquired():
raise 'Not Found', 'The requested resource does not exist.'
depth=REQUEST.get_header('Depth', 'infinity')
dest=REQUEST.get_header('Destination', '')
if not dest: raise 'Bad Request', 'No destination given'
......@@ -261,7 +230,7 @@ class Resource:
except 'Not Found':
raise 'Conflict', 'The resource %s must exist.' % path
except: raise sys.exc_type, sys.exc_value
if hasattr(parent, '_isNullResource'):
if hasattr(parent, '__dav_null__'):
raise 'Conflict', 'The resource %s must exist.' % path
if self.dav__is_acquired(parent):
raise 'Conflict', 'The resource %s must exist.' % path
......@@ -296,8 +265,6 @@ class Resource:
if not hasattr(aq_base(self), 'cb_isMoveable') or \
not self.cb_isMoveable():
raise 'Method Not Allowed', 'This object may not be moved.'
if self.dav__is_acquired():
raise 'Not Found', 'The requested resource does not exist.'
dest=REQUEST.get_header('Destination', '')
if not dest: raise 'Bad Request', 'No destination given'
flag=REQUEST.get_header('Overwrite', 'F')
......@@ -311,7 +278,7 @@ class Resource:
except 'Not Found':
raise 'Conflict', 'The resource %s must exist.' % path
except: raise sys.exc_type, sys.exc_value
if hasattr(parent, '_isNullResource'):
if hasattr(parent, '__dav_null__'):
raise 'Conflict', 'The resource %s must exist.' % path
if self.dav__is_acquired(parent):
raise 'Conflict', 'The resource %s must exist.' % path
......@@ -377,25 +344,3 @@ class Lock:
'<d:href>opaquelocktoken:%(token)s</d:href>\n' \
'</d:locktoken>\n' \
'</d:activelock>\n' % self.__dict__
def absattr(attr):
if callable(attr):
return attr()
return attr
def aq_base(ob):
if hasattr(ob, 'aq_base'):
return ob.aq_base
return ob
def rfc1123_date(ts=None):
# Return an RFC 1123 format date string, required for
# use in HTTP Date headers per the HTTP 1.1 spec.
if ts is None: ts=time.time()
ts=time.asctime(time.gmtime(ts))
ts=string.split(ts)
return '%s, %s %s %s %s GMT' % (ts[0],ts[2],ts[1],ts[3],ts[4])
......@@ -84,7 +84,10 @@
##############################################################################
"""The webdav package provides WebDAV class 1 functionality within
the Zope environment. Based on RFC 2518."""
the Zope environment. Based on:
__version__='$Revision: 1.2 $'[11:-2]
[WebDAV] Y. Y. Goland, E. J. Whitehead, Jr., A. Faizi, S. R. Carter, D.
Jensen, "HTTP Extensions for Distributed Authoring - WebDAV." RFC 2518.
Microsoft, U.C. Irvine, Netscape, Novell. February, 1999."""
__version__='$Revision: 1.3 $'[11:-2]
This diff is collapsed.
##############################################################################
#
# Zope Public License (ZPL) Version 0.9.4
# ---------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions in source code must retain the above
# copyright notice, this list of conditions, and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions, and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# 3. Any use, including use of the Zope software to operate a
# website, must either comply with the terms described below
# under "Attribution" or alternatively secure a separate
# license from Digital Creations.
#
# 4. All advertising materials, documentation, or technical papers
# mentioning features derived from or use of this software must
# display the following acknowledgement:
#
# "This product includes software developed by Digital
# Creations for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# 5. Names associated with Zope or Digital Creations must not be
# used to endorse or promote products derived from this
# software without prior written permission from Digital
# Creations.
#
# 6. Redistributions of any form whatsoever must retain the
# following acknowledgment:
#
# "This product includes software developed by Digital
# Creations for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# 7. Modifications are encouraged but must be packaged separately
# as patches to official Zope releases. Distributions that do
# not clearly separate the patches from the original work must
# be clearly labeled as unofficial distributions.
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND
# ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
# SHALL DIGITAL CREATIONS OR ITS CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
# THE POSSIBILITY OF SUCH DAMAGE.
#
# Attribution
#
# Individuals or organizations using this software as a web site
# must provide attribution by placing the accompanying "button"
# and a link to the accompanying "credits page" on the website's
# main entry point. In cases where this placement of
# attribution is not feasible, a separate arrangment must be
# concluded with Digital Creations. Those using the software
# for purposes other than web sites must provide a corresponding
# attribution in locations that include a copyright using a
# manner best suited to the application environment.
#
# This software consists of contributions made by Digital
# Creations and many individuals on behalf of Digital Creations.
# Specific attributions are listed in the accompanying credits
# file.
#
##############################################################################
"""Commonly used functions for WebDAV support modules."""
__version__='$Revision: 1.1 $'[11:-2]
import string, time
def absattr(attr):
if callable(attr):
return attr()
return attr
def aq_base(ob):
if hasattr(ob, 'aq_base'):
return ob.aq_base
return ob
def urlfix(url, s):
n=len(s)
if url[-n:]==s: url=url[:-n]
if len(url) > 1 and url[-1]=='/':
url=url[:-1]
return url
def is_acquired(ob):
# Return true if this object is not a direct
# subobject of its aq_parent object.
if not hasattr(ob, 'aq_parent'):
return 0
if hasattr(aq_base(ob.aq_parent), absattr(ob.id)):
return 0
if hasattr(aq_base(ob), 'isTopLevelPrincipiaApplicationObject'):
return 0
return 1
def rfc1123_date(ts=None):
# Return an RFC 1123 format date string, required for
# use in HTTP Date headers per the HTTP 1.1 spec.
if ts is None: ts=time.time()
ts=time.asctime(time.gmtime(ts))
ts=string.split(ts)
return '%s, %s %s %s %s GMT' % (ts[0],ts[2],ts[1],ts[3],ts[4])
......@@ -83,24 +83,20 @@
#
##############################################################################
"""WebDAV XML request objects."""
__version__='$Revision: 1.1 $'[11:-2]
"""WebDAV xml request objects."""
import sys, os, string, xmllib
__version__='$Revision: 1.2 $'[11:-2]
import sys, os, string
from common import absattr, aq_base, urlfix
from xmltools import XmlParser
from cStringIO import StringIO
zope_id='http://www.zope.org/propsets/default'
dav_id='DAV:'
def compact(self, data):
root=XmlParser().parse(data)
class PropFind:
"""Model a PROPFIND request."""
def __init__(self, request):
self.request=request
data=request.get('BODY', '')
......@@ -110,17 +106,17 @@ class PropFind:
self.propnames=[]
self.parse(data)
def parse(self, data):
def parse(self, data, dav='DAV:'):
if not data: return
root=XmlParser().parse(data)
e=root.elements('propfind', ns=dav_id)[0]
if e.elements('allprop', ns=dav_id):
e=root.elements('propfind', ns=dav)[0]
if e.elements('allprop', ns=dav):
self.allprop=1
return
if e.elements('propname', ns=dav_id):
if e.elements('propname', ns=dav):
self.propname=1
return
prop=e.elements('prop', ns=dav_id)[0]
prop=e.elements('prop', ns=dav)[0]
for val in prop.elements():
self.propnames.append((val.name(), val.namespace()))
return
......@@ -129,19 +125,15 @@ class PropFind:
if result is None:
result=StringIO()
depth=self.depth
url=self.request['URL']
if url[-9:]=='/PROPFIND':
url=url[:-9]
url=urlfix(self.request['URL'], 'PROPFIND')
result.write('<?xml version="1.0" encoding="utf-8"?>\n' \
'<d:multistatus xmlns:d="DAV:" ' \
'xmlns:z="%s">\n' % zope_id)
'<d:multistatus xmlns:d="DAV:">\n'
iscol=hasattr(aq_base(obj), 'isAnObjectManager') and \
obj.isAnObjectManager
if iscol and url[-1] != '/': url=url+'/'
result.write('<d:response>\n<d:href>%s</d:href>\n' % url)
if hasattr(obj, '__propsets__'):
for ps in obj.propertysheets.items():
for ps in obj.propertysheets.values():
if hasattr(aq_base(ps), 'dav__propstat'):
stat=ps.dav__propstat(self.allprop, self.propnames)
result.write(stat)
......@@ -160,18 +152,19 @@ class PropFind:
class PropPatch:
"""Model a PROPPATCH request."""
def __init__(self, request):
self.request=request
data=request.get('BODY', '')
self.values=[]
self.parse(data)
def parse(self, data):
def parse(self, data, dav='DAV:'):
root=XmlParser().parse(data)
e=root.elements('propertyupdate', ns=dav_id)[0]
e=root.elements('propertyupdate', ns=dav)[0]
for ob in e.elements():
if ob.name()=='set' and ob.namespace()==dav_id:
prop=ob.elements('prop', ns=dav_id)[0]
if ob.name()=='set' and ob.namespace()==dav:
prop=ob.elements('prop', ns=dav)[0]
for val in prop.elements():
# We have to ensure that all tag attrs (including
# an xmlns attr for all xml namespaces used by the
......@@ -183,25 +176,23 @@ class PropPatch:
md={'attrs':attrs, 'nsid': val.__nskey__}
item=(val.name(), val.namespace(), val.strval(), md)
self.values.append(item)
if ob.name()=='remove' and ob.namespace()==dav_id:
prop=ob.elements('prop', ns=dav_id)[0]
if ob.name()=='remove' and ob.namespace()==dav:
prop=ob.elements('prop', ns=dav)[0]
for val in prop.elements():
item=(val.name(), val.namespace())
self.values.append(item)
def apply(self, obj):
url=self.request['URL']
if url[-10:]=='/PROPPATCH':
url=url[:-10]
url=urlfix(self.request['URL'], 'PROPPATCH')
if hasattr(aq_base(obj), 'isAnObjectManager') and \
obj.isAnObjectManager and url[-1] != '/':
url=url+'/'
result=StringIO()
errors=[]
result.write('<?xml version="1.0" encoding="utf-8"?>\n' \
'<d:multistatus xmlns:d="DAV:" xmlns:z="%s">\n' \
'<d:multistatus xmlns:d="DAV:">\n' \
'<d:response>\n' \
'<d:href>%s</d:href>\n' % (zope_id, url))
'<d:href>%s</d:href>\n' % url)
propsets=obj.propertysheets
for value in self.values:
status='200 OK'
......@@ -210,7 +201,7 @@ class PropPatch:
propset=propsets.get(ns, None)
if propset is None:
obj.propertysheets.manage_addPropertySheet('', ns)
propsets=obj.propertysheets.items()
propsets=obj.propertysheets.values()
propset=propsets.get(ns)
if propset.hasProperty(name):
try: propset._updateProperty(name, val, meta=md)
......@@ -234,9 +225,9 @@ class PropPatch:
errors.append('%s cannot be deleted.' % name)
status='409 Conflict'
if result != '200 OK': abort=1
result.write('<d:propstat xmlns:ps="%s">\n' \
result.write('<d:propstat xmlns:n="%s">\n' \
' <d:prop>\n' \
' <ps:%s/>\n' \
' <n:%s/>\n' \
' </d:prop>\n' \
' <d:status>HTTP/1.1 %s</d:status>\n' \
'</d:propstat>\n' % (ns, name, status))
......@@ -256,117 +247,21 @@ class PropPatch:
class Lock:
def __init__(self, data):
"""Model a LOCK request."""
def __init__(self, request):
self.request=request
data=request.get('BODY', '')
self.scope='exclusive'
self.type='write'
self.owner=''
self.parse(data)
def parse(self, data):
def parse(self, data, dav='DAV:'):
root=XmlParser().parse(data)
info=root.elements('lockinfo', ns=dav_id)[0]
ls=info.elements('lockscope', ns=dav_id)[0]
info=root.elements('lockinfo', ns=dav)[0]
ls=info.elements('lockscope', ns=dav)[0]
self.scope=ls.elements()[0].name()
lt=info.elements('locktype', ns=dav_id)[0]
lt=info.elements('locktype', ns=dav)[0]
self.type=lt.elements()[0].name()
lo=info.elements('owner', ns=dav_id)
lo=info.elements('owner', ns=dav)
if lo: self.owner=lo[0].toxml()
def absattr(attr):
if callable(attr):
return attr()
return attr
def aq_base(ob):
if hasattr(ob, 'aq_base'):
return ob.aq_base
return ob
propfind_xml="""<?xml version="1.0" encoding="utf-8" ?>
<d:propfind xmlns:d="DAV:">
<d:prop xmlns:z="http://www.zope.org/propsets/default">
<z:title/>
<z:author/>
<z:content_type/>
</d:prop>
</d:propfind>
"""
rem_xml="""<?xml version="1.0" encoding="utf-8"?>
<d:propertyupdate xmlns:d="DAV:"
xmlns:z="http://www.zope.org/propsets/default">
<d:remove>
<d:prop>
<z:author/>
<z:title/>
</d:prop>
</d:remove>
</d:propertyupdate>
"""
proppatch_xml="""<?xml version="1.0" encoding="utf-8" ?>
<d:propertyupdate xmlns:d="DAV:"
xmlns:z="http://www.w3.com/standards/z39.50/">
<d:set>
<d:prop>
<z:authors>
<z:Author>Jim Whitehead</z:Author>
<z:Author>Roy Fielding</z:Author>
</z:authors>
</d:prop>
</d:set>
<d:remove>
<d:prop><z:Copyright-Owner/></d:prop>
</d:remove>
</d:propertyupdate>
"""
lock_xml="""<?xml version="1.0" encoding="utf-8" ?>
<D:lockinfo xmlns:D='DAV:'>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
<D:owner>
<D:href>http://www.ics.uci.edu/~ejw/contact.html</D:href>
</D:owner>
</D:lockinfo>
"""
multistatus_xml="""<?xml version="1.0" encoding="utf-8" ?>
<multistatus xmlns="DAV:">
<response xmlns:z="http://www.zope.org/dav/">
<href>http://www.foo.bar/container/</href>
<propstat>
<prop xmlns:R="http://www.foo.bar/boxschema/">
<R:bigbox z:type="int"/>
<R:author/>
<creationdate/>
<displayname/>
<resourcetype/>
<supportedlock/>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>http://www.foo.bar/container/front.html</href>
<propstat>
<prop xmlns:R="http://www.foo.bar/boxschema/">
<R:bigbox/>
<creationdate/>
<displayname/>
<getcontentlength/>
<getcontenttype/>
<getetag/>
<getlastmodified/>
<resourcetype/>
<supportedlock/>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>
"""
......@@ -84,7 +84,8 @@
##############################################################################
"""WebDAV XML parsing tools."""
__version__='$Revision: 1.1 $'[11:-2]
__version__='$Revision: 1.2 $'[11:-2]
import sys, os, string, xmllib
from Acquisition import Implicit
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment