Recently we came across a problem with our SharePoint 2010 (SP1) system (that we’ve been upgrading from SharePoint 2007), where we use a custom work-flow process to clean up a set of files once the work-flow has been completed.
To achieve this we use a timer job that is kicked off periodically which removes the files associated with any newly completed work-flows.
The issue we had is that even though the users are completing the work flow, which means they have finished with the files we have no way of being sure they have closed all the files they were working on as part of the workflow, files maybe left open in office or in some situations office may have crashed! In both these circumstances files are left with short term locks as though they are being editied and can’t be deleted, yes you can wait for the default short term lock timeout (in the crash scenario) however this won’t help you if the user still has the file open, as Office will continue to renew its short term lock.
Our solution to this problem when we were on SharePoint 2007 was to make an RPC call via HTTP to the author.dll, this enabled our workflow clean-up timer job to mimic the same RPC over HTTP call Office makes when you close a document this releases the lock on any file (it’s amazing what you can find out with fiddler). This worked fine for us on SP2007, however on SP2010, although it didn’t error if failed to unlock the files, so the delete process in the timer job couldn’t tidy up the documents.
We spent some time digging around inside the SharePoint 2010 assemblies (reflector to the rescue), looking into the content databases etc.., trying to spot any differences in internal implementation between SP2007 & SP2010, that might give us a clue as to why our RPC call over HTTP was no longer working.
In conjunction with a bit of SQL tracing we found the stored procedure proc_UncheckoutDocument within SharePoint’s content database that unlocks files when the RPC method on SP2010 is invoked, this procedure contains logic that checks to make sure the SharePoint user id passed to the proc matches the user who has the document checked out, and in our case this isn’t the case as the SharePoint\System user is attempting to delete the documents (our timer jobs run under this context), this procedure has a forceunlock parameter, that if set changes an internal logic path in the procedure to allow the unlock to happen regardless , interestingly the HTTP RPC call to the author.dll also had this parameter. So we thought no problem, lets just use the force unlock, however it appears that a small bug exists as when we had SQL tracing on we could never get the call via RPC to pass the forceunlock parameter down the the stored proc in the DB, it was always being passed as 0, even with the force unlock parameter set to 1 in the RPC call.
Our next port of call led us to a new API on the SPFile object for SharePoint 2010, ReleaseLock this appeared to offer what we wanted but again we ran into the same problem as the RPC method, our timer process wasn’t running under the user context so it failed to unlock the file, just to test things we tried calling the function manually while running a test component under the user context who had a file checked out or short term locked and it does indeed release the lock.
We next thought we need to impersonate the user who has the file open, trouble is this isn’t possible as we don’t hold the user credentials of all our users to create the runtime identity object from within our timer service.
After a little bit of headscratching, how we were going to solve this problem?
Our first clue was hmm hang on these user ID’s are SharePoint user ID’s from the userinfo table, not Windows identities, is there anyway we could create an SPSite context inside our timer job that uses this user ID rather than that of SharePoint\System?
Thankfully the SPSite object has a constructor that can take a SPUserToken object (I assume for this exact purpose) hmm what if we set this to the user who has the file checked out, we can do that by first getting the checked out SPUser from the SPFile.LockedByUser property, then use this to read the UserToken value we can do all this without needing to know the users credentials, then use this to create another SPSite with the correct user context, but will it work…
After a few minutes of development you can create a piece of prototype code, we do need to have two SPSite objects one from the Timer Service context SPWebApplication which will be associated with the SharePoint\System account to obtain the UserToken of the locked file, then another SPSite object created using the UserToken so we can unlock the file.
So if your stuggling to unlock files in a document library on SharePoint 2010 that others have left checked out, and you need to do it under the context of an account other than the user that has the file locked out, for example a timer job, give this approach a go it worked for us.
I take no responsibility for irate users, if you try this method to forcibly unlock a file while somebody really is still working on it, as they will likely lose the last set of changes!
You may find other methods mentioned on the internet about ways to do this by directly manipulating the tables in the SharePoint content database, such as this I would not recommend that you follow this approach, as interacting with and updating the databases directly in SharePoint may put you in breach of the EULA you signed up to when you installed SharePoint, and is definitly unsupported by Microsoft see here, so if you do it on a procduction system and break it your on your own, be warned !!