Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Z
Zope
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Kirill Smelkov
Zope
Commits
9c8f4d5e
Commit
9c8f4d5e
authored
Apr 28, 2006
by
Lennart Regebro
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Merge of the traversal refactoring branch.
parent
c4cceb84
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
305 additions
and
134 deletions
+305
-134
doc/CHANGES.txt
doc/CHANGES.txt
+11
-0
lib/python/OFS/Traversable.py
lib/python/OFS/Traversable.py
+34
-8
lib/python/Products/PageTemplates/Expressions.py
lib/python/Products/PageTemplates/Expressions.py
+41
-3
lib/python/ZPublisher/BaseRequest.py
lib/python/ZPublisher/BaseRequest.py
+219
-123
No files found.
doc/CHANGES.txt
View file @
9c8f4d5e
...
...
@@ -49,6 +49,17 @@ Zope Changes
Features added
- The traversal has been refactored to take heed of Zope3s
IPublishTraverse adapter interfaces. The ZCML directives
five:traversable and five:defaultViewable are therefore no
longer needed, as everything now is five:traversable and
five:defaultViewable.
There was a bug in earlier versions of Five that allowed you
to do custom publishing traversal with ITraversable adapters.
This bug has been corrected. Anybody using ITraversable
adapters need to convert them to IPublishTraversal adapters.
- Testing.makerequest: Added an 'environ' argument so
clients can use mappings other than os.environ.
...
...
lib/python/OFS/Traversable.py
View file @
9c8f4d5e
...
...
@@ -25,9 +25,14 @@ from AccessControl.ZopeGuards import guarded_getattr
from
Acquisition
import
Acquired
,
aq_inner
,
aq_parent
,
aq_base
from
zExceptions
import
NotFound
from
ZODB.POSException
import
ConflictError
from
zope.interface
import
implements
from
zope.interface
import
implements
,
Interface
from
interfaces
import
ITraversable
from
zope.app.traversing.interfaces
import
ITraversable
as
IZope3Traversable
from
zope.component
import
queryMultiAdapter
from
zope.app.traversing.interfaces
import
TraversalError
from
zope.app.traversing.namespace
import
nsParse
from
zope.app.traversing.namespace
import
namespaceLookup
_marker
=
object
()
...
...
@@ -59,6 +64,7 @@ class Traversable:
return
self
.
virtual_url_path
()
spp
=
self
.
getPhysicalPath
()
try
:
toUrl
=
self
.
REQUEST
.
physicalPathToURL
except
AttributeError
:
...
...
@@ -133,7 +139,6 @@ class Traversable:
If true, then all of the objects along the path are validated with
the security machinery. Usually invoked using restrictedTraverse().
"""
if
not
path
:
return
self
...
...
@@ -188,7 +193,19 @@ class Traversable:
continue
bobo_traverse
=
_getattr
(
obj
,
'__bobo_traverse__'
,
_none
)
if
bobo_traverse
is
not
_none
:
if
name
and
name
[:
1
]
in
'@+'
:
# Process URI segment parameters.
ns
,
nm
=
nsParse
(
name
)
if
ns
:
try
:
next
=
namespaceLookup
(
ns
,
nm
,
obj
,
self
.
REQUEST
).
__of__
(
obj
)
if
restricted
and
not
securityManager
.
validate
(
obj
,
obj
,
name
,
next
):
raise
Unauthorized
,
name
except
TraversalError
:
raise
AttributeError
(
name
)
elif
bobo_traverse
is
not
_none
:
next
=
bobo_traverse
(
REQUEST
,
name
)
if
restricted
:
if
aq_base
(
next
)
is
not
next
:
...
...
@@ -228,11 +245,20 @@ class Traversable:
next
=
_getattr
(
obj
,
name
,
marker
)
if
next
is
marker
:
try
:
next
=
obj
[
name
]
except
AttributeError
:
# Raise NotFound for easier debugging
# instead of AttributeError: __getitem__
raise
NotFound
,
name
try
:
next
=
obj
[
name
]
except
AttributeError
:
# Raise NotFound for easier debugging
# instead of AttributeError: __getitem__
raise
NotFound
,
name
except
(
NotFound
,
KeyError
):
# Try to look for a view
next
=
queryMultiAdapter
((
obj
,
self
.
REQUEST
),
Interface
,
name
)
if
next
is
None
:
# Didn't find one, reraise the error:
raise
next
=
next
.
__of__
(
obj
)
if
restricted
and
not
securityManager
.
validate
(
obj
,
obj
,
_none
,
next
):
raise
Unauthorized
,
name
...
...
lib/python/Products/PageTemplates/Expressions.py
View file @
9c8f4d5e
...
...
@@ -248,12 +248,28 @@ class NotExpr:
def
__repr__
(
self
):
return
'not:%s'
%
`self._s`
from
zope.interface
import
Interface
,
implements
from
zope.component
import
queryMultiAdapter
from
zope.app.traversing.namespace
import
nsParse
from
zope.app.traversing.namespace
import
namespaceLookup
from
zope.app.traversing.interfaces
import
TraversalError
from
zope.publisher.interfaces.browser
import
IBrowserRequest
from
zope.app.publication.browser
import
setDefaultSkin
class
FakeRequest
(
dict
):
implements
(
IBrowserRequest
)
def
getURL
(
self
):
return
"http://codespeak.net/z3/five"
def
restrictedTraverse
(
object
,
path
,
securityManager
,
get
=
getattr
,
has
=
hasattr
,
N
=
None
,
M
=
[],
TupleType
=
type
(())
):
REQUEST
=
{
'path'
:
path
}
REQUEST
=
FakeRequest
()
REQUEST
[
'path'
]
=
path
REQUEST
[
'TraversalRequestNameStack'
]
=
path
=
path
[:]
# Copy!
setDefaultSkin
(
REQUEST
)
path
.
reverse
()
validate
=
securityManager
.
validate
__traceback_info__
=
REQUEST
...
...
@@ -282,7 +298,20 @@ def restrictedTraverse(object, path, securityManager,
continue
t
=
get
(
object
,
'__bobo_traverse__'
,
N
)
if
t
is
not
N
:
if
name
and
name
[:
1
]
in
'@+'
:
import
pdb
pdb
.
set_trace
()
# Process URI segment parameters.
ns
,
nm
=
nsParse
(
name
)
if
ns
:
try
:
o
=
namespaceLookup
(
ns
,
nm
,
object
,
REQUEST
).
__of__
(
object
)
if
not
validate
(
object
,
object
,
name
,
o
):
raise
Unauthorized
,
name
except
TraversalError
:
raise
AttributeError
(
name
)
elif
t
is
not
N
:
o
=
t
(
REQUEST
,
name
)
container
=
None
...
...
@@ -305,7 +334,16 @@ def restrictedTraverse(object, path, securityManager,
# XXX maybe in Python 2.2 we can just check whether
# the object has the attribute "__getitem__"
# instead of blindly catching exceptions.
o
=
object
[
name
]
try
:
o
=
object
[
name
]
except
(
AttributeError
,
KeyError
):
# Try to look for a view
o
=
queryMultiAdapter
((
object
,
REQUEST
),
Interface
,
name
)
if
o
is
None
:
# Didn't find one, reraise the error:
raise
o
=
o
.
__of__
(
object
)
except
AttributeError
,
exc
:
if
str
(
exc
).
find
(
'__getitem__'
)
>=
0
:
# The object does not support the item interface.
...
...
lib/python/ZPublisher/BaseRequest.py
View file @
9c8f4d5e
...
...
@@ -16,10 +16,22 @@ $Id$
"""
from
urllib
import
quote
import
xmlrpc
from
zExceptions
import
Forbidden
from
zExceptions
import
Forbidden
,
Unauthorized
,
NotFound
from
zope.interface
import
implements
,
providedBy
,
Interface
from
zope.component
import
queryMultiAdapter
from
zope.component
import
getSiteManager
from
zope.component.interfaces
import
ComponentLookupError
from
zope.event
import
notify
from
zope.app.publication.interfaces
import
EndRequestEvent
from
zope.app.publisher.browser
import
queryDefaultViewName
from
zope.publisher.interfaces
import
IPublishTraverse
from
zope.component.interfaces
import
IDefaultViewName
from
zope.publisher.interfaces.browser
import
IBrowserPublisher
from
zope.publisher.interfaces.browser
import
IBrowserRequest
from
zope.app.traversing.interfaces
import
TraversalError
from
zope.app.traversing.namespace
import
nsParse
from
zope.app.traversing.namespace
import
namespaceLookup
UNSPECIFIED_ROLES
=
''
...
...
@@ -45,6 +57,86 @@ except ImportError:
def
getRoles
(
container
,
name
,
value
,
default
):
return
getattr
(
value
,
'__roles__'
,
default
)
class
DefaultPublishTraverse
(
object
):
implements
(
IBrowserPublisher
)
def
__init__
(
self
,
context
,
request
):
self
.
context
=
context
self
.
request
=
request
def
publishTraverse
(
self
,
request
,
name
):
object
=
self
.
context
URL
=
request
[
'URL'
]
if
name
[:
1
]
==
'_'
:
raise
Forbidden
(
"Object name begins with an underscore at: %s"
%
URL
)
try
:
if
hasattr
(
object
,
'__bobo_traverse__'
):
subobject
=
object
.
__bobo_traverse__
(
request
,
name
)
if
type
(
subobject
)
is
type
(())
and
len
(
subobject
)
>
1
:
# Add additional parents into the path
# XXX This needs handling. Check the publish refactor branch...
parents
[
-
1
:]
=
list
(
subobject
[:
-
1
])
object
,
subobject
=
subobject
[
-
2
:]
else
:
try
:
subobject
=
getattr
(
object
,
name
)
except
AttributeError
:
subobject
=
object
[
name
]
except
(
AttributeError
,
KeyError
,
NotFound
):
# Find a view even if it doesn't start with @@, but only
# If nothing else could be found
subobject
=
queryMultiAdapter
((
object
,
request
),
Interface
,
name
)
if
subobject
is
not
None
:
# OFS.Application.__bobo_traverse__ calls
# REQUEST.RESPONSE.notFoundError which sets the HTTP
# status code to 404
request
.
RESPONSE
.
setStatus
(
200
)
# We don't need to do the docstring security check
# for views, so lets skip it and return the object here.
return
subobject
.
__of__
(
object
)
raise
# Ensure that the object has a docstring, or that the parent
# object has a pseudo-docstring for the object. Objects that
# have an empty or missing docstring are not published.
doc
=
getattr
(
subobject
,
'__doc__'
,
None
)
if
doc
is
None
:
doc
=
getattr
(
object
,
'%s__doc__'
%
name
,
None
)
if
not
doc
:
raise
Forbidden
(
"The object at %s has an empty or missing "
\
"docstring. Objects must have a docstring to be "
\
"published."
%
URL
)
# Hack for security: in Python 2.2.2, most built-in types
# gained docstrings that they didn't have before. That caused
# certain mutable types (dicts, lists) to become publishable
# when they shouldn't be. The following check makes sure that
# the right thing happens in both 2.2.2+ and earlier versions.
if
not
typeCheck
(
subobject
):
raise
Forbidden
(
"The object at %s is not publishable."
%
URL
)
return
subobject
def
browserDefault
(
self
,
request
):
if
hasattr
(
self
.
context
,
'__browser_default__'
):
return
self
.
context
.
__browser_default__
(
request
)
# Zope 3.2 still uses IDefaultView name when it
# registeres default views, even though it's
# deprecated. So we handle that here:
default_name
=
queryDefaultViewName
(
self
.
context
,
request
)
if
default_name
is
not
None
:
return
self
.
context
,
(
default_name
,)
return
self
.
context
,
()
_marker
=
[]
class
BaseRequest
:
...
...
@@ -184,6 +276,35 @@ class BaseRequest:
__repr__
=
__str__
def
traverseName
(
self
,
ob
,
name
):
if
name
and
name
[:
1
]
in
'@+'
:
# Process URI segment parameters.
ns
,
nm
=
nsParse
(
name
)
if
ns
:
try
:
ob2
=
namespaceLookup
(
ns
,
nm
,
ob
,
self
)
except
TraversalError
:
raise
KeyError
(
ob
,
name
)
return
ob2
.
__of__
(
ob
)
if
name
==
'.'
:
return
ob
if
IPublishTraverse
.
providedBy
(
ob
):
ob2
=
ob
.
publishTraverse
(
self
,
name
)
else
:
adapter
=
queryMultiAdapter
((
ob
,
self
),
IPublishTraverse
)
if
adapter
is
None
:
## Zope2 doesn't set up its own adapters in a lot of cases
## so we will just use a default adapter.
adapter
=
DefaultPublishTraverse
(
ob
,
self
)
ob2
=
adapter
.
publishTraverse
(
self
,
name
)
return
ob2
def
traverse
(
self
,
path
,
response
=
None
,
validated_hook
=
None
):
"""Traverse the object space
...
...
@@ -193,7 +314,6 @@ class BaseRequest:
request
=
self
request_get
=
request
.
get
if
response
is
None
:
response
=
self
.
response
debug_mode
=
response
.
debug_mode
# remember path for later use
browser_path
=
path
...
...
@@ -235,14 +355,14 @@ class BaseRequest:
object
=
parents
[
-
1
]
del
parents
[:]
roles
=
getRoles
(
None
,
None
,
object
,
UNSPECIFIED_ROLES
)
self
.
roles
=
getRoles
(
None
,
None
,
object
,
UNSPECIFIED_ROLES
)
# if the top object has a __bobo_traverse__ method, then use it
# to possibly traverse to an alternate top-level object.
if
hasattr
(
object
,
'__bobo_traverse__'
):
try
:
object
=
object
.
__bobo_traverse__
(
request
)
roles
=
getRoles
(
None
,
None
,
object
,
UNSPECIFIED_ROLES
)
self
.
roles
=
getRoles
(
None
,
None
,
object
,
UNSPECIFIED_ROLES
)
except
:
pass
if
not
path
and
not
method
:
...
...
@@ -277,125 +397,101 @@ class BaseRequest:
path
=
request
.
path
=
request
[
'TraversalRequestNameStack'
]
# Check for method:
if
path
:
entry_name
=
path
.
pop
()
el
if
hasattr
(
object
,
'__browser_default__'
)
:
# If we have reached the end of the path
. W
e look to see
# if
the object implements __browser_default__. If so, we
# call it to let the object tell us how to publish it
#
__browser_default__
returns the object to be published
entry_name
=
path
.
pop
()
el
se
:
# If we have reached the end of the path
, w
e look to see
# if
we can find IBrowserPublisher.browserDefault. If so,
#
we
call it to let the object tell us how to publish it
#
BrowserDefault
returns the object to be published
# (usually self) and a sequence of names to traverse to
# find the method to be published. (Casey)
request
.
_hacked_path
=
1
object
,
default_path
=
object
.
__browser_default__
(
request
)
if
len
(
default_path
)
>
1
:
path
=
list
(
default_path
)
method
=
path
.
pop
()
request
[
'TraversalRequestNameStack'
]
=
path
continue
# find the method to be published.
if
(
IBrowserPublisher
.
providedBy
(
object
)
or
IDefaultViewName
.
providedBy
(
object
)):
adapter
=
object
else
:
entry_name
=
default_path
[
0
]
elif
(
method
and
hasattr
(
object
,
method
)
and
entry_name
!=
method
and
getattr
(
object
,
method
)
is
not
None
):
request
.
_hacked_path
=
1
entry_name
=
method
method
=
'index_html'
else
:
if
(
hasattr
(
object
,
'__call__'
)):
roles
=
getRoles
(
object
,
'__call__'
,
object
.
__call__
,
roles
)
if
request
.
_hacked_path
:
i
=
URL
.
rfind
(
'/'
)
if
i
>
0
:
response
.
setBase
(
URL
[:
i
])
break
adapter
=
queryMultiAdapter
((
object
,
self
),
IBrowserPublisher
)
if
adapter
is
None
:
# Zope2 doesn't set up its own adapters in a lot
# of cases so we will just use a default adapter.
adapter
=
DefaultPublishTraverse
(
object
,
self
)
newobject
,
default_path
=
adapter
.
browserDefault
(
self
)
if
default_path
or
newobject
is
not
object
:
object
=
newobject
request
.
_hacked_path
=
1
if
len
(
default_path
)
>
1
:
path
=
list
(
default_path
)
method
=
path
.
pop
()
request
[
'TraversalRequestNameStack'
]
=
path
continue
else
:
entry_name
=
default_path
[
0
]
elif
(
method
and
hasattr
(
object
,
method
)
and
entry_name
!=
method
and
getattr
(
object
,
method
)
is
not
None
):
request
.
_hacked_path
=
1
entry_name
=
method
method
=
'index_html'
else
:
if
hasattr
(
object
,
'__call__'
):
self
.
roles
=
getRoles
(
object
,
'__call__'
,
object
.
__call__
,
self
.
roles
)
if
request
.
_hacked_path
:
i
=
URL
.
rfind
(
'/'
)
if
i
>
0
:
response
.
setBase
(
URL
[:
i
])
break
step
=
quote
(
entry_name
)
_steps
.
append
(
step
)
request
[
'URL'
]
=
URL
=
'%s/%s'
%
(
request
[
'URL'
],
step
)
got
=
0
if
entry_name
[:
1
]
==
'_'
:
if
debug_mode
:
try
:
subobject
=
self
.
traverseName
(
object
,
entry_name
)
if
(
hasattr
(
object
,
'__bobo_traverse__'
)
or
hasattr
(
object
,
entry_name
)):
check_name
=
entry_name
else
:
check_name
=
None
self
.
roles
=
getRoles
(
object
,
check_name
,
subobject
,
self
.
roles
)
object
=
subobject
except
(
KeyError
,
AttributeError
):
if
response
.
debug_mode
:
return
response
.
debugError
(
"Object name begins with an underscore at: %s"
%
URL
)
else
:
return
response
.
forbiddenError
(
entry_name
)
if
hasattr
(
object
,
'__bobo_traverse__'
):
try
:
subobject
=
object
.
__bobo_traverse__
(
request
,
entry_name
)
if
type
(
subobject
)
is
type
(())
and
len
(
subobject
)
>
1
:
# Add additional parents into the path
parents
[
-
1
:]
=
list
(
subobject
[:
-
1
])
object
,
subobject
=
subobject
[
-
2
:]
except
(
AttributeError
,
KeyError
):
if
debug_mode
:
return
response
.
debugError
(
"Cannot locate object at: %s"
%
URL
)
else
:
return
response
.
notFoundError
(
URL
)
else
:
try
:
# Note - no_acquire_flag is necessary to support
# things like DAV. We have to make sure
# that the target object is not acquired
# if the request_method is other than GET
# or POST. Otherwise, you could never use
# PUT to add a new object named 'test' if
# an object 'test' existed above it in the
# heirarchy -- you'd always get the
# existing object :(
if
(
no_acquire_flag
and
len
(
path
)
==
0
and
hasattr
(
object
,
'aq_base'
)):
if
hasattr
(
object
.
aq_base
,
entry_name
):
subobject
=
getattr
(
object
,
entry_name
)
else
:
raise
AttributeError
,
entry_name
else
:
subobject
=
getattr
(
object
,
entry_name
)
except
AttributeError
:
got
=
1
try
:
subobject
=
object
[
entry_name
]
except
(
KeyError
,
IndexError
,
TypeError
,
AttributeError
):
if
debug_mode
:
return
response
.
debugError
(
"Cannot locate object at: %s"
%
URL
)
else
:
return
response
.
notFoundError
(
URL
)
# Ensure that the object has a docstring, or that the parent
# object has a pseudo-docstring for the object. Objects that
# have an empty or missing docstring are not published.
doc
=
getattr
(
subobject
,
'__doc__'
,
None
)
if
doc
is
None
:
doc
=
getattr
(
object
,
'%s__doc__'
%
entry_name
,
None
)
if
not
doc
:
return
response
.
debugError
(
"The object at %s has an empty or missing "
\
"docstring. Objects must have a docstring to be "
\
"published."
%
URL
)
# Hack for security: in Python 2.2.2, most built-in types
# gained docstrings that they didn't have before. That caused
# certain mutable types (dicts, lists) to become publishable
# when they shouldn't be. The following check makes sure that
# the right thing happens in both 2.2.2+ and earlier versions.
if
not
typeCheck
(
subobject
):
return
response
.
debugError
(
"The object at %s is not publishable."
%
URL
)
roles
=
getRoles
(
object
,
(
not
got
)
and
entry_name
or
None
,
subobject
,
roles
)
# Promote subobject to object
object
=
subobject
"Cannot locate object at: %s"
%
URL
)
else
:
return
response
.
notFoundError
(
URL
)
except
Forbidden
,
e
:
if
self
.
response
.
debug_mode
:
return
response
.
debugError
(
e
.
args
)
else
:
return
response
.
forbiddenError
(
entry_name
)
parents
.
append
(
object
)
steps
.
append
(
entry_name
)
finally
:
parents
.
reverse
()
# Note - no_acquire_flag is necessary to support
# things like DAV. We have to make sure
# that the target object is not acquired
# if the request_method is other than GET
# or POST. Otherwise, you could never use
# PUT to add a new object named 'test' if
# an object 'test' existed above it in the
# heirarchy -- you'd always get the
# existing object :(
if
(
no_acquire_flag
and
hasattr
(
parents
[
1
],
'aq_base'
)
and
not
hasattr
(
parents
[
1
],
'__bobo_traverse__'
)):
if
not
(
hasattr
(
parents
[
1
].
aq_base
,
entry_name
)
or
parents
[
1
].
aq_base
.
has_key
(
entry_name
)):
raise
AttributeError
,
entry_name
# After traversal post traversal hooks aren't available anymore
del
self
.
_post_traverse
...
...
@@ -427,25 +523,25 @@ class BaseRequest:
auth
=
request
.
_auth
if
v
is
old_validation
and
roles
is
UNSPECIFIED_ROLES
:
if
v
is
old_validation
and
self
.
roles
is
UNSPECIFIED_ROLES
:
# No roles, so if we have a named group, get roles from
# group keys
if
hasattr
(
groups
,
'keys'
):
roles
=
groups
.
keys
()
if
hasattr
(
groups
,
'keys'
):
self
.
roles
=
groups
.
keys
()
else
:
try
:
groups
=
groups
()
except
:
pass
try
:
roles
=
groups
.
keys
()
try
:
self
.
roles
=
groups
.
keys
()
except
:
pass
if
groups
is
None
:
# Public group, hack structures to get it to validate
roles
=
None
self
.
roles
=
None
auth
=
''
if
v
is
old_validation
:
user
=
old_validation
(
groups
,
request
,
auth
,
roles
)
elif
roles
is
UNSPECIFIED_ROLES
:
user
=
v
(
request
,
auth
)
else
:
user
=
v
(
request
,
auth
,
roles
)
user
=
old_validation
(
groups
,
request
,
auth
,
self
.
roles
)
elif
self
.
roles
is
UNSPECIFIED_ROLES
:
user
=
v
(
request
,
auth
)
else
:
user
=
v
(
request
,
auth
,
self
.
roles
)
while
user
is
None
and
i
<
last_parent_index
:
parent
=
parents
[
i
]
...
...
@@ -456,11 +552,11 @@ class BaseRequest:
if
hasattr
(
groups
,
'validate'
):
v
=
groups
.
validate
else
:
v
=
old_validation
if
v
is
old_validation
:
user
=
old_validation
(
groups
,
request
,
auth
,
roles
)
elif
roles
is
UNSPECIFIED_ROLES
:
user
=
v
(
request
,
auth
)
else
:
user
=
v
(
request
,
auth
,
roles
)
user
=
old_validation
(
groups
,
request
,
auth
,
self
.
roles
)
elif
self
.
roles
is
UNSPECIFIED_ROLES
:
user
=
v
(
request
,
auth
)
else
:
user
=
v
(
request
,
auth
,
self
.
roles
)
if
user
is
None
and
roles
!=
UNSPECIFIED_ROLES
:
if
user
is
None
and
self
.
roles
!=
UNSPECIFIED_ROLES
:
response
.
unauthorized
()
if
user
is
not
None
:
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment