Bulk Office Protection Application
Adds another layer of security to your Word or Excel documents. Can search for and bulk process documents to add a protection password or change your protection password.
Introduction
This project is an upgrade of the Bulk Word Protection Utility posted here. Most of it's been re-written or re-factored since that article. It's a C# application used to search for and loop through all Word or Excel files and change protection settings in bulk.
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.
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
There is a FileSearchClass
class, a ProcessDocuments
class, and some UI code behind the OfficeAppProtectionForm
form.
On the OfficeAppProtectionForm
form, there are five regions of code:
- Form Events: Contains the
InitializeComponent
and selects a document type on load. - Click Events: All [control]
_Click
events. - UI Events: Miscellaneous UI events. Many call other methods on the form.
- BackgroundWorker Events: Contains the
_DoWork
,_ProgressChanged
, and_RunWorkerCompleted
events for the twoBackgroundWorker
processes. - Private Methods: Just a few grunts to do my UI bidding.
I'll spare you the mundane UI code and get into the BackgroundWorker
s and classes. First up, the xSearchFolderBackground
BackgroundWorker
. I will link a separate article just for this class. It's kind of cool by my standards since it estimates the number of sub-directories, accepts a BackgroundWorker
from the calling form, and sends updates back to it, and you can have a somewhat accurate ProgressBar
.
When you click the xSelectDirectory
button and choose a directory to search, the selected directory and the search pattern for the selected document type get appended together and passed to the xSearchFolderBackground.RunWorkerAsync
method. Then, the xSearchFolderBackground_DoWork
event, which is now running on the BackgroundWorker
's thread, creates a new FileSearchClass
, sends the BackgroundWorker
to the class, splits apart the directory and search pattern, and starts the search. I'll show the details of the FileSearchClass
in the other article. Here's the code for the click event and the BackgroundWorker
events:
private void xSelectDirectory_Click(object sender, EventArgs e)
// search the selected directory for the file type chosen
{
this.DoubleBuffered = true;
if (this.xFolderBrowserDialog.ShowDialog() == DialogResult.OK)
{
// if the background worker is not already doing stuff, kick it off
if (xSearchFolderBackground.IsBusy != true)
{
if (this.xFilesListBox.Items.Count > 0)
if (MessageBox.Show("Would you like to clear the already " +
"added filenames before searching?",
"Files Already Listed",
MessageBoxButtons.YesNo) == DialogResult.Yes)
{
this.xFilesListBox.Items.Clear();
}
string searchPattern = string.Empty;
if (this.xDocTypeComboBox.Text == "Word Documents")
searchPattern = "*.doc";
else
searchPattern = "*.xls";
this.xDocProgressBar.Visible = true;
ChangeControlStatus(false);
this.Refresh();
xSearchFolderBackground.RunWorkerAsync(
this.xFolderBrowserDialog.SelectedPath +
"|" + searchPattern);
}
}
}
private void xSearchFolderBackground_DoWork(object sender, DoWorkEventArgs e)
/// create a FileSearchClass and pass the directory and search pattern
/// to the search method.
{
string[] args = (e.Argument as string).Split("|".ToCharArray());
string directoryToSearch = args[0];
string searchPattern = args[1];
FileSearchClass searchClass = new FileSearchClass();
searchClass.bw = (BackgroundWorker)sender;
searchClass.Search(directoryToSearch, searchPattern);
}
private void xSearchFolderBackground_ProgressChanged(object sender,
ProgressChangedEventArgs e)
// whenever the background process progress changes,
// update the progress bar on the form
{
if (this.xDocProgressBar.Maximum >= e.ProgressPercentage)
{
this.xDocProgressBar.Value = e.ProgressPercentage;
}
else
{
//debug...shouldn't be greater than 100% progress!
}
if (e.UserState != null && e.UserState is FileSearchClass.UserState)
{
FileSearchClass.UserState state = (FileSearchClass.UserState)e.UserState;
if (state.currentOperation == FileSearchClass.Operations.Estimating)
this.xStatusLbl.Text =
string.Format("Status: {0}", state.estimatingMessage);
else if (state.currentOperation == FileSearchClass.Operations.Searching)
{
///***
///I commented this section out.
///The refresh of the label is so fast that
///it won't let you click the Cancel button!
///***
// so the directory path doesn't flow off the edge of the form:
//if (state.directoryInProcess.Length > 80)
// state.directoryInProcess =
// state.directoryInProcess.Substring(0, 56) +
// "..." +
// state.directoryInProcess.Substring(
// state.directoryInProcess.Length - 20, 20);
//this.xStatusLbl.Text = string.Format("Status: {0}",
// state.directoryInProcess);
this.xStatusLbl.Text = string.Format("Status: {0}",
state.searchingMessage);
//this.xStatusLbl.Refresh();
if (state.foundFileName != string.Empty)
{
this.xFilesListBox.Items.Add(state.foundFileName);
this.xFilesListBox.Refresh();
}
}
}
}
private void xSearchFolderBackground_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
if (!(e.Error == null))
{
MessageBox.Show(e.Error.Message, "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else
{
// re-enable controls on form
ChangeControlStatus(true);
// update the number of filenames selected
UpdateNumListboxItems();
this.xStatusLbl.Text = string.Format("Status: {0}", "Search complete!");
}
}
The ProcessDocuments
class does all of the heavy lifting when bulk processing the documents. The xGoButton
click event gathers all the UI information, sends it to a new ProcessDocuments
class, and passes the class to the xProcessDocsBackground.RunWorkerAsync
method:
private void xGoButton_Click(object sender, EventArgs e)
// Starts/cancels document processing or cancels the search process.
{
// if the user hit the button while it says
// "Cancel", cancel the background worker
if (this.xGoButton.Text == "Cancel" &&
xProcessDocsBackground.WorkerSupportsCancellation &&
xProcessDocsBackground.IsBusy == true)
{
xProcessDocsBackground.CancelAsync();
ChangeControlStatus(true);
return;
}
// if the user hit the button while it says
// "Cancel", cancel the background worker
if (this.xGoButton.Text == "Cancel" &&
xSearchFolderBackground.WorkerSupportsCancellation &&
xSearchFolderBackground.IsBusy == true)
{
xSearchFolderBackground.CancelAsync();
ChangeControlStatus(true);
return;
}
// build the list of filenames
ArrayList fileNames = new ArrayList();
fileNames.AddRange(this.xFilesListBox.Items);
// build the list of passwords
ArrayList userPasswords = new ArrayList();
if (this.xUnprotectPW.Text.Trim() == string.Empty)
{
userPasswords.AddRange(this.xUnprotectPW.Items);
}
else
{
userPasswords.Add(this.xUnprotectPW.Text.Trim());
userPasswords.AddRange(this.xUnprotectPW.Items);
}
// get WdProtectionType chosen
WdProtectionType userWdProtectionType;
if (xNoProtectionRB.Checked)
userWdProtectionType = WdProtectionType.wdNoProtection;
else if (xTrackedChangesRB.Checked)
userWdProtectionType = WdProtectionType.wdAllowOnlyRevisions;
else if (xReadOnlyRB.Checked)
userWdProtectionType = WdProtectionType.wdAllowOnlyReading;
else if (xFillingFormsRB.Checked)
userWdProtectionType = WdProtectionType.wdAllowOnlyFormFields;
else
userWdProtectionType = WdProtectionType.wdAllowOnlyReading;
// create a class to hold data needed to pass to the background worker
ProcessDocuments newDocInfoClass = new ProcessDocuments
{
fileNames = fileNames.ToArray(typeof(string)) as string[],
reprotectPassword = this.xProtectPW.Text,
passwords = userPasswords.ToArray(typeof(string)) as string[],
suggReadOnly = this.xReadOnlyCB.Checked,
wordProtectionType = userWdProtectionType,
bw = xProcessDocsBackground,
wordDocsToProcess = this.xDocTypeComboBox.Text.Contains("Word"),
xlDocsToProcess = this.xDocTypeComboBox.Text.Contains("Excel"),
removeXlProtection = this.xExcelNoProtectionRB.Checked
};
// if the background worker is not already doing stuff, kick it off
if (xProcessDocsBackground.IsBusy != true)
{
xProcessDocsBackground.RunWorkerAsync(newDocInfoClass);
ChangeControlStatus(false);
}
}
private void xProcessDocsBackground_DoWork(object sender, DoWorkEventArgs e)
// process all documents in the list in the background
{
ProcessDocuments passedDocInfo = e.Argument as ProcessDocuments;
passedDocInfo.ProcessOfficeDocs();
}
In the ProcessDocuments
class, I check to see if there are Word documents to process or Excel documents to process, and run either ProcessWordDocs()
or ProcessExcelDocs()
.
Those methods start an instance of either Word or Excel, 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.
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 ProcessWordDocs()
// Processes all Word documents
{
if (_bw != null && _bw.WorkerReportsProgress)
{
// report progress back to form after every document has been processed
_bw.ReportProgress(_bwPercentComplete, "Opening Word...");
}
// create an instance of MS Word and a reference to a Document object
Word.Application wordApp = new Word.Application();
Word.Document doc;
// 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 = fileNames[i];
if (_bw != null && _bw.WorkerReportsProgress)
{
// report progress back to form after every document has been processed
_bwPercentComplete = (int)Math.Round((numFilesProcessed / _numFiles) * 100);
_bw.ReportProgress(_bwPercentComplete, "Processing '" +
filename + "'");
}
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 != Word.WdProtectionType.wdNoProtection)
{
try
{
// first, try to unprotect it with an empty password
object blankPassword = string.Empty;
doc.Unprotect(ref blankPassword);
}
catch { } // drop through errors to try the entered password
// loop through every password in the list and try to unprotect it
for (int j = 0; j < _passwords.GetLength(0); j++)
{
try
{
object unprotectPassword = _passwords[j];
doc.Unprotect(ref unprotectPassword);
}
catch { } // drop through errors and try the next password
}
}
// if the document is unprotected, re-protect it according to user settings
if (doc.ProtectionType == Word.WdProtectionType.wdNoProtection)
{
// protect the document according to the selected protection type
doc.Protect(wordProtectionType, 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 _suggReadOnly,
ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing);
// close the document so you can delete the original file
((Word._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);
}
}
else
/// couldn't unprotect the document, so 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;
// now close the document
((Word._Document)doc).Close(ref objFalse, ref missing, ref missing);
}
}
catch (Exception) // exception while opening file
{
// 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);
}
// check to see if the user hit the Cancel button
if (_bw != null && _bw.CancellationPending)
{
// close Word
((Word._Application)wordApp).Quit(ref objFalse, ref missing, ref missing);
// write logs out to disk
WriteLogs(errorFileNames, filesSavedSuccessfully,
numErrorFiles, numFilesSaved);
//bail out
return;
}
if (_bw != null && _bw.WorkerReportsProgress)
{
// report progress back to form after every document has been processed
_bwPercentComplete = (int)Math.Round((numFilesProcessed / _numFiles) * 100);
_bw.ReportProgress(_bwPercentComplete);
}
/// 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)
{
if (MessageBox.Show(numErrorFiles +
" operations have failed out of " +
numFilesProcessed + ". Would you like to quit?",
"Operations Failed", MessageBoxButtons.YesNo,
MessageBoxIcon.Information) == DialogResult.Yes)
{
// close Word
((Word._Application)wordApp).Quit(ref objFalse,
ref missing, ref missing);
// write logs out to disk
WriteLogs(errorFileNames, filesSavedSuccessfully,
numErrorFiles, numFilesSaved);
// bail out
return;
}
}
// reset the counter for the next batch of 10
counter = 0;
}
}
// We've processed all of the files, so quit Word
((Word._Application)wordApp).Quit(ref objFalse, ref missing, ref missing);
// write logs out to disk
WriteLogs(errorFileNames, filesSavedSuccessfully, numErrorFiles, numFilesSaved);
}
private void ProcessExcelDocs()
// Processes all Excel documents
{
if (_bw != null && _bw.WorkerReportsProgress)
{
// report progress back to form after every document has been processed
_bw.ReportProgress(_bwPercentComplete, "Opening Excel...");
}
// create an instance of MS Word and a reference to a Document object
Excel.Application xlApp = new Excel.Application();
Excel.Workbook xlWorkbook;
xlApp.DisplayAlerts = true;
xlApp.DisplayInfoWindow = 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++)
{
string filename = fileNames[i];
if (_bw != null && _bw.WorkerReportsProgress)
{
// report progress back to form after every document has been processed
_bwPercentComplete = (int)Math.Round((numFilesProcessed / _numFiles) * 100);
_bw.ReportProgress(_bwPercentComplete, "Processing '" +
filename + "'");
}
try
{
// open the specified Excel document
xlWorkbook = xlApp.Workbooks.Open(filename, missing, objFalse, missing,
missing, missing, objTrue, missing, missing, missing,
missing, missing, missing, missing, missing);
// loop through each worksheet in the excel document and unprotect it
foreach (Excel.Worksheet xlWorksheet in xlWorkbook.Sheets)
{
if (xlWorksheet.ProtectContents == true)
{
// loop through every password
// in the list box and try to unprotect it
for (int j = 0; j < _passwords.GetLength(0); j++)
{
try
{
xlWorksheet.Unprotect(_passwords[j]);
}
catch { } // drop through errors and try the next password
}
}
}
bool noErrors = true;
foreach (Excel.Worksheet xlWorksheet in xlWorkbook.Sheets)
{
// if the file is unprotected and the user wants to protect it, do so.
if (xlWorksheet.ProtectContents == false)
{
if (!_removeXlProtection)
{
// protect the document according to the selected protection type
xlWorksheet.Protect(_reprotectPassword, objTrue, objTrue,
objTrue, missing, objFalse, objFalse, objFalse,
objFalse, objFalse, objFalse, objFalse,
objFalse, objFalse, objFalse, objFalse);
}
}
else
//store the filename for an error report and break for the next file
{
errorFileNames = errorFileNames + filename +
System.Environment.NewLine;
numFilesProcessed = numFilesProcessed + 1;
numErrorFiles = numErrorFiles + 1;
// now close the document
xlWorkbook.Close(objFalse, missing, objFalse);
noErrors = false;
break;
}
}
if (noErrors == true)
{
/// 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 + ".temp";
try
{
// save the doc with the temporary filename and close the document
xlWorkbook.SaveAs(tempFilename, missing, missing, missing,
_suggReadOnly, missing, Excel.XlSaveAsAccessMode.xlNoChange,
missing, missing, missing, missing, missing);
// close the workbook so you can delete the original file
xlWorkbook.Close(objFalse, missing, objFalse);
// 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) // exception while opening file
{
// 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);
}
// check to see if the user hit the Cancel button
if (_bw != null && _bw.CancellationPending)
{
// close Excel
try
{
xlApp.Workbooks.Close();
xlApp.Quit();
}
catch (Exception) { }
KillExcel();
// write logs out to disk
WriteLogs(errorFileNames, filesSavedSuccessfully,
numErrorFiles, numFilesSaved);
//bail out
return;
}
if (_bw != null && _bw.WorkerReportsProgress)
{
// report progress back to form after every document has been processed
_bwPercentComplete = (int)Math.Round((numFilesProcessed / _numFiles) * 100);
_bw.ReportProgress(_bwPercentComplete);
}
/// 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)
{
if (MessageBox.Show(numErrorFiles +
" operations have failed out of " +
numFilesProcessed + ". Would you like to quit?",
"Failing Operations", MessageBoxButtons.YesNo,
MessageBoxIcon.Information) == DialogResult.Yes)
{
// close Excel
try
{
xlApp.Workbooks.Close();
xlApp.Quit();
}
catch (Exception) { }
KillExcel();
// write logs out to disk
WriteLogs(errorFileNames, filesSavedSuccessfully,
numErrorFiles, numFilesSaved);
// bail out
return;
}
}
// reset the counter for the next batch of 10
counter = 0;
}
}
// We've processed all of the files, so quit Excel
try
{
xlApp.Workbooks.Close();
xlApp.Quit();
}
catch (Exception) { }
KillExcel();
// write logs out to disk
WriteLogs(errorFileNames, filesSavedSuccessfully, numErrorFiles, numFilesSaved);
}
I ran in to a problem closing Excel which took me quite a while to figure out. Basically, you have to jump through a bunch of hoops to get Excel to release all of its COM resources, and it won't close until everything has been properly released.
So after hours of trying to make sure everything was closed properly, I finally just strong-armed Excel into closing by finding the instance of Excel.exe running in the Task Manager and closing the process with a blank MainWindowTitle
. Regular instances of Excel running have something in the MainWindowTitle
, so it will leave other open instances of Excel alone:
private void KillExcel()
/// kills the instance of "Excel" process in which the Window title is empty.
/// I had to do this because I couldn't get the Excel COM resources to release properly.
{
foreach (Process xlProcess in Process.GetProcessesByName("Excel"))
{
//when started programmatically the window title
//is empty, won't kill other Excel instances
if (xlProcess.MainWindowTitle == string.Empty)
xlProcess.Kill();
}
}
Then finally, the logs get written to the C:\ directory and are launched automatically:
private void WriteLogs(string errorFileNames, string filesSavedSuccessfully,
double numErrorFiles, double numFilesSaved)
// Write log files out to disk and opens them
{
// error log and protection log filenames
const string errorsFileDir = "c:\\errors.log";
const string reportFileDir = "c:\\successes.log";
System.IO.FileStream fileStream;
// Now we create the log reports
try // to write the summary log file to disk
{
fileStream = System.IO.File.Create(reportFileDir);
System.IO.StreamWriter objWriter;
objWriter = new System.IO.StreamWriter(fileStream);
objWriter.Write(filesSavedSuccessfully +
"______________________________________________" +
System.Environment.NewLine + numFilesSaved.ToString() +
" file(s) processed successfully.");
objWriter.Close();
MessageBox.Show("Success log created at: " + reportFileDir,
"Log Written", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception)
{
MessageBox.Show("Could not create report log at: " + reportFileDir,
"Log Written", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
try // to write the error log file to disk
{
fileStream = System.IO.File.Create(errorsFileDir);
System.IO.StreamWriter objWriter;
objWriter = new System.IO.StreamWriter(fileStream);
objWriter.Write(errorFileNames +
"______________________________________________" +
System.Environment.NewLine + numErrorFiles.ToString() + " problem file(s).");
objWriter.Close();
MessageBox.Show("Error log created at: " + errorsFileDir,
"Log Written", MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
catch (Exception)
{
MessageBox.Show("Could not create error log at: " + errorsFileDir,
"Log Written", MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
// open the summary log
ProcessStartInfo procStartInfo = new ProcessStartInfo();
procStartInfo.FileName = reportFileDir;
Process.Start(procStartInfo);
// open the error log
procStartInfo.FileName = errorsFileDir;
Process.Start(procStartInfo);
}
Points of Interest
Like I said, you could modify this application to include other document properties to mass modify. The document subject, author, keywords, revision number, or any other document properties which are editable.
The search class is very reusable, and you can find out more about it here. It doesn't search inside ZIP files, and doesn't include shortcuts, so the number of files you find won't match exactly what your OS will find.
History
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.
My previous article, Bulk Word Processing Utility, obviously could only do Word documents, and the search functionality froze the whole program. I've also done a bunch of re-factoring and performance tuning, and now it should be a bit faster and more resilient.
The program is now compatible with Office 2010.