Plone Lock Manager, controlling lock timeouts
=============================================

Here we have some tests for the lock behavior on Zope. It doesn't
follow the spec 100%, but close enough to be usable. Plone Lock
Manager allows you to control the maximum and default timeout of locks
through the Plone UI, site-wide (per-site).

First, let's create two users for this test:

  >>> user1, pass1 = 'user1', 'pass1'
  >>> user2, pass2 = 'user2', 'pass2'
  >>> uf = self.portal.acl_users
  >>> uf.userFolderAddUser(user1, pass1, ['Manager'], [])
  >>> uf.userFolderAddUser(user2, pass2, ['Manager'], [])

Check that we actually have a Document as index_html:

  >>> not 'index_html' in self.portal.objectIds()
  True

  >>> _ = self.portal.invokeFactory('Document', 'index_html')
  >>> print self.portal.index_html.getPortalTypeName()
  Document

Simple locking with first user
------------------------------

First user gets a lock. Response includes the lock token in header and
also as part of the lockdiscover/activelock properties. By default the
lock is set to 720 seconds (about 12 minutes).

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user1</owner></lockinfo>
  ... """ % (user1, pass1))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  Lock-Token: opaquelocktoken:...00105A989226...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user1</d:owner>
    <d:timeout>Second-720</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Let's fetch the lock token programatically to ease our job and try to
send another lock request including the lock token on the If:
header. As we are providing a body in this request, Zope will consider
that it's a normal lock request and reply with a 423 Locked response
code.

  >>> token = self.portal.index_html.wl_lockValues()[0].getLockToken()

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... If: <http://localhost:9090/plone/index_html> (<opaquelocktoken:%s>)
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype></lockinfo>
  ... """ % (user1, pass1, token))
  HTTP/1.1 423 Locked
  Accept-Ranges: none
  ...
  <BLANKLINE>

Refreshing a lock
-----------------

However, if we send a lock request with no body, and including the
lock token, Zope will consider it a lock refresh request and refresh
the lock validity. Note though that this response doesn't include the
Lock-Token header, but includes the timeout, which is still set to 720
seconds.

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... If: <http://localhost:9090/plone/index_html> (<opaquelocktoken:%s>)
  ... Te: trailers
  ... """ % (user1, pass1, token))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user1</d:owner>
    <d:timeout>Second-720</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Stealing (or not!) a lock
-------------------------

Now, let's try to acquire a lock using a different user while the
first user still holds the lock. As expected, the user should receive
a 423 Locked response.

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 423 Locked
  Accept-Ranges: none
  ...
  <BLANKLINE>

Doing a PROPFIND request using this other user to get the active
locks, will return a fake token to prevent the user from stealing the
lock.

  >>> print http(r"""
  ... PROPFIND /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <propfind xmlns="DAV:"><prop>
  ... <lockdiscovery xmlns="DAV:"/>
  ... </prop></propfind>
  ... """ % (user2, pass2))
  HTTP/1.1 207 Multi-Status
  Accept-Ranges: none
  ...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8"?>
  <d:multistatus xmlns:d="DAV:">
  <d:response>
  <d:href>/plone/index_html</d:href>
  <d:propstat>
    <d:prop>
  <n:lockdiscovery xmlns:n="DAV:">
  <BLANKLINE>
   <n:activelock>
    <n:locktype><n:write/></n:locktype>
    <n:lockscope><n:exclusive/></n:lockscope>
    <n:depth>0</n:depth>
    <n:owner>user1</n:owner>
    <n:timeout>Second-720</n:timeout>
    <n:locktoken>
     <n:href>opaquelocktoken:this-is-a-faked-no-permission-token</n:href>
    </n:locktoken>
   </n:activelock>
  <BLANKLINE>
  </n:lockdiscovery>
    </d:prop>
    <d:status>HTTP/1.1 200 OK</d:status>
  </d:propstat>
  </d:response>
  </d:multistatus>

Trying to unlock using this faked token will not have any effect,
however Zope still returns a 204 response status which seems
incorrect, though I can't figure out from the spec what is the correct
answer that should be given in this case. I'm inclined to say it
should be a 423 Locked.

  >>> print http(r"""
  ... UNLOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Lock-Token: <opaquelocktoken:this-is-a-faked-no-permission-token>
  ... Te: trailers
  ... """ % (user2, pass2))
  HTTP/1.1 204 No Content
  Accept-Ranges: none
  ...
  <BLANKLINE>

To confirm that the lock hasn't been removed we can do two things: One
is to look directly in Zope for existing locks:

  >>> len(self.portal.index_html.wl_lockValues())
  1

And the other one is to try to lock the item with this user. It should
fail with a 423 Locked response status.

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 423 Locked
  Accept-Ranges: none
  ...
  <BLANKLINE>

Checking for existing locks
---------------------------

Now, back to the first user, we can issue a lockdiscovery request to
see what are the active lock tokens. As this user is the creator of
the only existing lock, he will be able to see the lock token just
fine.

  >>> print http(r"""
  ... PROPFIND /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <propfind xmlns="DAV:"><prop>
  ... <lockdiscovery xmlns="DAV:"/>
  ... </prop></propfind>
  ... """ % (user1, pass1))
  HTTP/1.1 207 Multi-Status
  Accept-Ranges: none
  ...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8"?>
  <d:multistatus xmlns:d="DAV:">
  <d:response>
  <d:href>/plone/index_html</d:href>
  <d:propstat>
    <d:prop>
  <n:lockdiscovery xmlns:n="DAV:">
  <BLANKLINE>
   <n:activelock>
    <n:locktype><n:write/></n:locktype>
    <n:lockscope><n:exclusive/></n:lockscope>
    <n:depth>0</n:depth>
    <n:owner>user1</n:owner>
    <n:timeout>Second-720</n:timeout>
    <n:locktoken>
     <n:href>opaquelocktoken:...00105A989226...</n:href>
    </n:locktoken>
   </n:activelock>
  <BLANKLINE>
  </n:lockdiscovery>
    </d:prop>
    <d:status>HTTP/1.1 200 OK</d:status>
  </d:propstat>
  </d:response>
  </d:multistatus>

Unlocking
---------

And should also be able to unlock the resource, given that the knows
the token.

  >>> print http(r"""
  ... UNLOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Lock-Token: <opaquelocktoken:%s>
  ... Te: trailers
  ... """ % (user1, pass1, token))
  HTTP/1.1 204 No Content
  Accept-Ranges: none
  ...
  <BLANKLINE>

Let's make sure the lock is gone:

  >>> len(self.portal.index_html.wl_lockValues())
  0

  >>> print http(r"""
  ... PROPFIND /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <propfind xmlns="DAV:"><prop>
  ... <lockdiscovery xmlns="DAV:"/>
  ... </prop></propfind>
  ... """ % (user1, pass1))
  HTTP/1.1 207 Multi-Status
  Accept-Ranges: none
  ...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8"?>
  <d:multistatus xmlns:d="DAV:">
  <d:response>
  <d:href>/plone/index_html</d:href>
  <d:propstat>
    <d:prop>
  <n:lockdiscovery xmlns:n="DAV:">
  <BLANKLINE>
  </n:lockdiscovery>
    </d:prop>
    <d:status>HTTP/1.1 200 OK</d:status>
  </d:propstat>
  </d:response>
  </d:multistatus>

Simple locking, second user
---------------------------

Now, back to the second user, let's lock the resource again:

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user2</owner></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  Lock-Token: opaquelocktoken:...00105A989226...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user2</d:owner>
    <d:timeout>Second-720</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Stealing the lock, this time for real
-------------------------------------

Supposing that the first user knows the lock token, he can easily
steal the lock, even not being the lock owner:

  >>> len(self.portal.index_html.wl_lockValues())
  1

  >>> token = self.portal.index_html.wl_lockValues()[0].getLockToken()

  >>> print http(r"""
  ... UNLOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Lock-Token: <opaquelocktoken:%s>
  ... Te: trailers
  ... """ % (user1, pass1, token))
  HTTP/1.1 204 No Content
  Accept-Ranges: none
  ...
  <BLANKLINE>

  >>> len(self.portal.index_html.wl_lockValues())
  0

No support for shared locks
---------------------------

Zope only accepts 'exclusive' 'write' locks.

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><shared/></lockscope>
  ... <locktype><write/></locktype></lockinfo>
  ... """ % (user1, pass1))
  HTTP/1.1 403 Forbidden
  ...

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><read/></locktype></lockinfo>
  ... """ % (user1, pass1))
  HTTP/1.1 403 Forbidden
  ...

Multiple locks, only possible programatically
---------------------------------------------

Now, though the HTTP/WebDAV interfaces prevent the creation of more
than one lock at a time, nothing prevents the creation of multiple
locks from inside Zope:

  >>> from webdav.LockItem import LockItem

  >>> creator = self.portal.acl_users.getUserById(user1)
  >>> lock = LockItem(creator)
  >>> token = lock.getLockToken()
  >>> self.portal.index_html.wl_setLock(token, lock)

  >>> creator = self.portal.acl_users.getUserById(user2)
  >>> lock = LockItem(creator, owner='user2')
  >>> token = lock.getLockToken()
  >>> self.portal.index_html.wl_setLock(token, lock)

  >>> len(self.portal.index_html.wl_lockValues())
  2

Checking for multiple existing locks
------------------------------------

Doing a lock discovery request as user1 should return both locks, but
for the second lock the token will be obscured as the user is not the
creator.

  >>> res = http(r"""
  ... PROPFIND /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <propfind xmlns="DAV:"><prop>
  ... <lockdiscovery xmlns="DAV:"/>
  ... </prop></propfind>
  ... """ % (user1, pass1))

The order that the tokens are returned may vary, account for this by
checking each one separatedly.

Check status:

  >>> print res
  HTTP/1.1 207 Multi-Status
  Accept-Ranges: none
  ...

Check that one of the tokens is owned by user2 and has a fake token:

  >>> print res
  HTTP/1.1 207 Multi-Status
  ...
  <BLANKLINE>
     <n:activelock>
    <n:locktype><n:write/></n:locktype>
    <n:lockscope><n:exclusive/></n:lockscope>
    <n:depth>0</n:depth>
    <n:owner>user2</n:owner>
    <n:timeout>Second-720</n:timeout>
    <n:locktoken>
     <n:href>opaquelocktoken:this-is-a-faked-no-permission-token</n:href>
    </n:locktoken>
   </n:activelock>
  <BLANKLINE>
  ...

Check that the other token is not fake:

  >>> print res
  HTTP/1.1 207 Multi-Status
  ...
  <BLANKLINE>
   <n:activelock>
    <n:locktype><n:write/></n:locktype>
    <n:lockscope><n:exclusive/></n:lockscope>
    <n:depth>0</n:depth>
    <n:owner></n:owner>
    <n:timeout>Second-720</n:timeout>
    <n:locktoken>
     <n:href>opaquelocktoken:...00105A989226...</n:href>
    </n:locktoken>
   </n:activelock>
  <BLANKLINE>
  ...

Setting a different timeout manually
------------------------------------

Clear existing locks:

  >>> self.portal.index_html.wl_clearLocks()

Create a lock specifying a timeout of 60 seconds, that's done by
setting a header:

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Timeout: Second-60
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user2</owner></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  Lock-Token: opaquelocktoken:...00105A989226...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user2</d:owner>
    <d:timeout>Second-60</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Clear existing locks:

  >>> self.portal.index_html.wl_clearLocks()

Create a lock specifying a timeout of 3600 seconds, that's done by
setting a header:

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Timeout: Second-3600
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user2</owner></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  Lock-Token: opaquelocktoken:...00105A989226...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user2</d:owner>
    <d:timeout>Second-3600</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Clear locks again:

  >>> self.portal.index_html.wl_clearLocks()

Funny thing is, you can give Zope pretty much anything in that header,
as long as it ends with '-<number-of-seconds>' Zope will accept it as
a timeout in seconds.

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Timeout: Years-12
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user2</owner></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  Lock-Token: opaquelocktoken:...00105A989226...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user2</d:owner>
    <d:timeout>Second-12</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Maximum lock timeout
--------------------

Clear existing locks:

  >>> self.portal.index_html.wl_clearLocks()

If the 'Timeout' header is set to 'infinite' or not provided, then
it is set to 720 seconds by default.

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Timeout: Infinite
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user2</owner></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  Lock-Token: opaquelocktoken:...00105A989226...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user2</d:owner>
    <d:timeout>Second-720</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Clear locks again:

  >>> self.portal.index_html.wl_clearLocks()

If the 'Timeout' header is set to a value bigger than the maximum
timeout (which is set to 2**32 - 1 (4294967295) seconds by default,
an error will occur:

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Timeout: Second-4294967296
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user2</owner></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 403 Forbidden
  Accept-Ranges: none
  ...

You can set the lock to the maximum allowed just fine.

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Timeout: Second-4294967295
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user2</owner></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  Lock-Token: opaquelocktoken:...00105A989226...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user2</d:owner>
    <d:timeout>Second-4294967295</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Controlling default and maximum lock timeout
--------------------------------------------

With PloneLockManager, you can control the existing and maximum lock
timeout. We'll set the default timeout to 1200 seconds (20 minutes)
and maximum timeout to 3600 seconds (one hour).

  >>> from Products.CMFCore.utils import getToolByName
  >>> from Products.PloneLockManager.config import TOOL_ID
  >>> tool = getToolByName(self.portal, TOOL_ID)
  >>> tool._updateProperty('default_lock_timeout', 1200)
  >>> tool._updateProperty('maximum_lock_timeout', 3600)

Clear existing locks:

  >>> self.portal.index_html.wl_clearLocks()

Create a lock specifying a timeout of 3600 seconds, that's done by
setting a header. This will still work because it's equal the maximum
timeout:

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Timeout: Second-3600
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user2</owner></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  Lock-Token: opaquelocktoken:...00105A989226...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user2</d:owner>
    <d:timeout>Second-3600</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Clear existing locks:

  >>> self.portal.index_html.wl_clearLocks()

Creating a lock specifying a timeout bigger than 3600 seconds, which
is now the maximum timeout will silently set the timeout to 3600
seconds instead without raising an exception:

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Timeout: Second-6000
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user2</owner></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  Lock-Token: opaquelocktoken:...00105A989226...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user2</d:owner>
    <d:timeout>Second-3600</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Clear existing locks:

  >>> self.portal.index_html.wl_clearLocks()

Creating a lock without specifying the timeout will set it to the new
default timeout, which is now 1200 seconds:

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Depth: 0
  ... Te: trailers
  ...
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user2</owner></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  Lock-Token: opaquelocktoken:...00105A989226...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user2</d:owner>
    <d:timeout>Second-1200</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>
