Bulk Word Protection Utility
Can be used to bulk search for and unprotect or protect Word documents. Can also be used to bulk change passwords for Word document protection.
Introduction
In a previous life, all of our Quality documents were Word documents which needed to be password protected to "Filling In Forms Only" using Word's document protection functionality. I was tasked with changing this password...for all 500+ documents on the server. So, I wrote a C# application to loop through all .doc(x) files and do this for me :p.
Of course, the real usefulness is that you can bulk reset properties of Word documents fairly easily. For now, it just works with document protection, but you could mass modify any number of document properties with only a few more lines of code and some more options in the GUI.
It would probably be possible to do this with Excel documents as well, but I've never had the need to check it out...yet.
Background
If you're not familiar with Word document protection, some info is available here: http://office.microsoft.com/en-us/word/CH010397751033.aspx.
Using the code
I wrote this in C# simply because I started out with VB.NET and wanted to force myself to get more familiar with C#. The code could easily be translated to VB.NET.
Almost all of my code is right behind the form except some startup checking in Main
and a DocInfoClass
used to pass information from the form to the BackgroundWorker
as an argument. When doing the recursive search, the FileSearch
method looks in the directory selected, and the DirSearch
method is the recursive part.
The bulk of the intelligence is in the ProcessDocsBackground_DoWork
sub which is called by the "Go" button. The "Go" button code handles both kicking off the background thread to process documents and also canceling the background worker.
private void xGoButton_Click(object sender, EventArgs e)
{
// if the user hit the button while it says "Cancel",
// cancel the background worker
if (this.xGoButton.Text == "Cancel" &&
ProcessDocsBackground.WorkerSupportsCancellation)
{
ProcessDocsBackground.CancelAsync();
ChangeControlStatus(true);
return;
}
// create a class to hold data needed to pass to the background worker
DocInfoClass newDocInfoClass = new DocInfoClass
{
fileNames = this.xFilesListBox,
numFiles = this.xFilesListBox.Items.Count,
numPasswords = this.xUnprotectPW.Items.Count,
unprotectPassword = this.xUnprotectPW.Text,
reprotectPassword = this.xProtectPW.Text,
passwords = this.xUnprotectPW,
suggReadOnly = this.xReadOnlyCB.Checked
};
// if the background worker is not already doing stuff, kick it off
if (ProcessDocsBackground.IsBusy != true)
{
this.xDocProgressBar.Visible = true;
ProcessDocsBackground.RunWorkerAsync(newDocInfoClass);
ChangeControlStatus(false);
}
}
The ProcessDocsBackgroun_DoWork
sub creates an instance of MS Word, opens each document in the list, tries every password in the list to unprotect the document (if necessary), and re-protects it according to the user's settings.
Finally, it deletes the original document and replaces it with the new unprotected version, and spits out a log file for successful documents and problematic documents.
During processing, after every 10 documents processed, the code checks to see if more than 50% of the documents are failing (wrong password, incorrect permissions, etc.), and asks the user if they want to stop.
private void ProcessDocsBackground_DoWork(object sender, DoWorkEventArgs e)
// process all documents in the list in the background
{
DocInfoClass passedDocInfo = e.Argument as DocInfoClass;
// create an instance of MS Word and a reference to a Document object
Microsoft.Office.Interop.Word.Application wordApp =
new Microsoft.Office.Interop.Word.Application();
Microsoft.Office.Interop.Word.Document doc;
// setup properties for document
object unprotectPassword = passedDocInfo.unprotectPassword;
object reprotectPassword = passedDocInfo.reprotectPassword;
object missing = System.Reflection.Missing.Value;
object objReadOnly = (object)passedDocInfo.suggReadOnly;
int numFiles = passedDocInfo.numFiles;
object empty = string.Empty;
object objFalse = false;
object objTrue = true;
// string to hold the names of every
// filename where an error occurred
string errorFileNames = null;
// string to hold the names of every
// filename which was processed successfully
string filesSavedSuccessfully = null;
// number of files where an error occurred
double numErrorFiles = 0;
// number of files successfully saved
double numFilesSaved = 0;
// total number of files processed, should be
// the same as the number of files in the list box
double numFilesProcessed = 0;
// counter will be reset after every 10 files processed
int counter = 0;
// for every item in the files list box,
// process the filename specified
for (int i = 0; i < numFiles; i++)
{
object filename = passedDocInfo.fileNames.Items[i];
try
{
// open the specified Word document
doc = wordApp.Documents.Open(ref filename, ref missing,
ref objFalse, ref missing, ref missing,
ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing, ref objFalse,
ref missing, ref missing, ref missing, ref missing);
// if the document is protected, try every
// password in the list to unprotect it
if (doc.ProtectionType != WdProtectionType.wdNoProtection)
{
if (this.xUnprotectPW.Items.Count == 0)
/// there's no items in the password listbox,
/// so try a blank password or whatever
/// was entered into the password text box
{
try
{
// first, try to unprotect it with an empty password
unprotectPassword = string.Empty;
doc.Unprotect(ref unprotectPassword);
}
catch { } // drop through errors to try the entered password
try
// try to unprotect it with whatever is in the password text box
{
unprotectPassword = passedDocInfo.unprotectPassword;
doc.Unprotect(ref unprotectPassword);
}
catch { } // drop through errors
}
else
/// there were passwords in the list box, so try blank, whatever
/// was entered in the text box, and each password in the list
{
try
{
// first, try to unprotect it with an empty password
unprotectPassword = string.Empty;
doc.Unprotect(ref unprotectPassword);
}
catch { } // drop through errors to try the entered password
try
// try to unprotect it with whatever is in the password text box
{
unprotectPassword = passedDocInfo.unprotectPassword;
doc.Unprotect(ref unprotectPassword);
}
catch { } // drop through errors
// loop through every password in the list box and try to unprotect it
for (int j = 0; j < passedDocInfo.numPasswords; j++)
{
try
{
unprotectPassword = passedDocInfo.passwords.Items[j].ToString();
doc.Unprotect(ref unprotectPassword);
}
catch { } // drop through errors and try the next password
}
}
}
/// see if the document is unprotected, if not, store the filename for
/// an error report and move to the next file
if (doc.ProtectionType != WdProtectionType.wdNoProtection)
{
errorFileNames = errorFileNames + filename + System.Environment.NewLine;
numFilesProcessed = numFilesProcessed + 1;
numErrorFiles = numErrorFiles + 1;
//MessageBox.Show("Could not unprotect the document: " + filename);
// now close the document
((_Document)doc).Close(ref objFalse, ref missing, ref missing);
}
else
/// the file is unprotected, so see what kind of protection the user wants
/// to put back on the file and protect it
/// accordingly with the password provided
{
if (xNoProtectionRB.Checked)
{
doc.Protect(WdProtectionType.wdNoProtection,
ref objFalse, ref reprotectPassword,
ref missing, ref missing);
}
else if (xTrackedChangesRB.Checked)
{
doc.Protect(WdProtectionType.wdAllowOnlyRevisions,
ref objFalse, ref reprotectPassword,
ref missing, ref missing);
}
else if (xReadOnlyRB.Checked)
{
doc.Protect(WdProtectionType.wdAllowOnlyReading,
ref objFalse, ref reprotectPassword, ref missing, ref missing);
}
else if (xFillingFormsRB.Checked)
{
doc.Protect(WdProtectionType.wdAllowOnlyFormFields,
ref objFalse, ref reprotectPassword, ref missing, ref missing);
}
else
{
doc.Protect(WdProtectionType.wdAllowOnlyReading,
ref objFalse, ref reprotectPassword, ref missing, ref missing);
}
/// I couldn't find a way to overwrite the file
/// without error, so I create a temporary filename,
/// save the new document, delete the original,
/// and rename the temporary file with the original filename
object tempFilename = filename.ToString() + ".temp";
try
{
// save the doc with the temporary filename and close the document
doc.SaveAs(ref tempFilename, ref missing, ref missing,
ref missing, ref objFalse, ref missing, ref objReadOnly,
ref missing, ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing, ref missing,
ref missing);
((_Document)doc).Close(ref objFalse, ref missing, ref missing);
// delete the original file
System.IO.File.Delete(filename.ToString());
// rename the temporary file with
// the original filename using the Move method
System.IO.File.Move(tempFilename.ToString(), filename.ToString());
// increment counters and store filename
// to be output into a log file later
numFilesSaved = numFilesSaved + 1;
numFilesProcessed = numFilesProcessed + 1;
filesSavedSuccessfully = filesSavedSuccessfully +
filename + System.Environment.NewLine;
}
catch (Exception)
// there was a problem somewhere
// on the save, delete, or rename operations
{
// log the filename to be output into
// the error log later and increment the counters
errorFileNames = errorFileNames + filename +
System.Environment.NewLine;
numFilesProcessed = numFilesProcessed + 1;
numErrorFiles = numErrorFiles + 1;
MessageBox.Show("Could not save the document: " + filename);
}
}
}
catch (Exception)
{ }// allow errors to drop through. maybe the file wouldn't open for some reason...
// check to see if the user hit the Cancel button
if (ProcessDocsBackground.CancellationPending)
{
// close Word
((_Application)wordApp).Quit(ref objFalse, ref missing, ref missing);
// write logs out to disk
WriteLogs(errorFileNames, filesSavedSuccessfully,
numErrorFiles, numFilesSaved);
/// set the cancelled property to true.
/// I check this property inside the
/// RunWorkerCompleted event
e.Cancel = true;
//bail out
return;
}
// report progress back to form after every document has been processed
int percentCompleted = (int)Math.Round((numFilesProcessed / numFiles)*100);
ProcessDocsBackground.ReportProgress((int)percentCompleted);
/// at this point, the document's been saved or has
/// errored out. either way it's been processed,
/// so increment the number of documents processed
counter = counter + 1;
//After every 10 files saved, if 50% or more
//of the operations are failing, prompt user for action
if (counter == 10)
{
double percentFailed = (numErrorFiles / numFilesProcessed);
if (percentFailed >= 0.5)
{
DialogResult bailOut = DialogResult;
if (MessageBox.Show(numErrorFiles +
" operations have failed out of " +
numFilesProcessed +
". Would you like to quit?",
"Protection Wizard", MessageBoxButtons.YesNo,
MessageBoxIcon.Information) == DialogResult.Yes)
{
// close Word
((_Application)wordApp).Quit(ref objFalse,
ref missing, ref missing);
// write logs out to disk
WriteLogs(errorFileNames, filesSavedSuccessfully,
numErrorFiles, numFilesSaved);
/// set the cancelled property to true.
/// I check this property inside the
/// RunWorkerCompleted event
e.Cancel = true;
// bail out
return;
}
}
// reset the counter for the next batch of 10
counter = 0;
}
}
// We've processed all of the files, so quit Word
((_Application)wordApp).Quit(ref objFalse, ref missing, ref missing);
// write logs out to disk
WriteLogs(errorFileNames, filesSavedSuccessfully, numErrorFiles, numFilesSaved);
}
The rest of the code is pretty self-explanatory and a lot of GUI code. About 20% of my code is comments, but if you have any questions, let me know.
Points of interest
It was fun figuring out how the BackgroundWorker
is used. And, getting it to take data from the form's thread and pass back data to the progress bar was interesting and challenging the first time you do it.
My inspiration for the method to access the Word properties came partly from here. Other than that, I've taken bits and pieces of information from the Web as I needed, mostly just concepts though and not actual code.
History
I've made a bunch of updates to it over the past few months, but this is the first release to the public. When I was searching for a tool to do this, I couldn't find anything, so I thought it might be worthwhile for someone out there (I hope). I figured it wouldn't take too long to throw an article up here and let someone reap the fruits of my labor (toil).
If anybody knows of any other (free) tool that does this for you, please let me know, I'd be interested to see what they do.