My SharePoint 2007 Custom Membership Provider Adventure

Sometime last year I wanted to set up a SharePoint 2007 website for my family members to exchange information. That being said, I was using a custom membership provider, and I ran into a few issues while I was setting things up. I had kept detailed notes while I was configuring my server and troubleshooting the problems that I encountered, and with that in mind, I thought that I would share my experiences. ;-]


Getting Started

Specifying My Environment

My web server is only an older 32-bit Windows Server 2008 computer, so I couldn't install SharePoint 2010 (which is 64-bit only) and I had to install SharePoint 2007. Taking that into account, there were a few additional considerations that I had for my environment:

  • I wanted to use Forms-Based Authentication (FBA). Even though I run my own active directory domain, I avoid giving out physical accounts if I don't have to, so FBA seemed like a great idea.
  • I didn't want to use the built-in ASP.NET membership and roles provider. This is for two reasons:
    • I was already using the built-in ASP.NET membership provider on other websites, and I didn't feel like researching whether I should share the membership database between SharePoint and my other websites, or if I should set up unique membership databases.
    • If you've been reading my previous blogs and you think that I'd be content with using a built-in provider, then you haven't been paying attention. Usually I find myself wanting to do things the hard way, and other times I simply want to write code, but either way I decided to use the sample read-only XML membership and role providers that I documented in the following article:
  • I decided that I could use FBA over HTTP, and therefore I didn't worry about setting up SSL. (I run my own certificate server, so I could have issued myself a certificate and given the root CA certificate to everyone; but this wasn't necessary, so I didn't bother with it.)

Researching My Scenario

With my specific considerations in mind, I took a look at the following article to get started:

That being said, I did not use the following articles, even though they are related to my scenario and they looked interesting:


Creating the SharePoint Website

Here are the brief details on how I created my SharePoint website:

  1. I followed the steps in the following walkthrough in order to create and register the read-only XML membership and role providers with IIS 7:
  2. I created the following physical paths for my website:
    • Website root folder: C:\Inetpub\SharePointSite\wwwroot
    • Application data folder: C:\Inetpub\SharePointSite\wwwroot\App_Data
  3. I created the following user/role XML file for my website:
    • I created an XML file in the location: C:\Inetpub\SharePointSite\wwwroot\App_Data\MyUsers.xml
    • I added the following XML to the file:
      <Users>
         <User>
            <UserName>Alice</UserName>
            <Password>P@ssw0rd</Password>
            <EMail>alice@contoso.com</EMail>
            <Roles>Admins</Roles>
         </User>
         <User>
            <UserName>Bob</UserName>
            <Password>P@ssw0rd</Password>
            <EMail>bob@contoso.com</EMail>
            <Roles>Authors</Roles>
         </User>
      </Users>
  4. I opened the Internet Information Services (IIS) Manager and created a new website; I used the C:\Inetpub\SharePointSite\wwwroot folder for the home directory.
  5. I opened SharePoint 3.0 Central Administration to convert my website into a SharePoint 2007 site:
    1. I clicked the Application Management tab, then clicked Create or extend Web application, and then clicked Create a new Web application:
      • In my case I chose Use an existing IIS web site because I had already created the website that I wanted to use.
      • I chose Create new application pool, I used "Network Service" for the identification, and then I specified all of the requisite database information.
    2. When that completed, I clicked the Application Management tab, and then clicked Create site collection:
      • I specified all options, and I used a valid Active Directory account as the administrator for now.
    3. Once the site was created, I modified the web.config file for the website and the SharePoint Central Administration web.config file. (See the following notes for the details.) Note: The SharePoint Central Administration website needs to know the information about your membership provider in order to add administrators.
    4. After that, I clicked the Application Management tab, and then clicked Authentication Providers:
      • I verified that I was using the correct "Web Application" in the drop-down menu.
      • I clicked on the Default zone.
      • I set the Authentication Type to Forms.
      • I specified the appropriate Membership provider name and Role manager name.
    5. When that completed, I needed to restart IIS before continuing. (NOTE: I used "iisreset" from a command line.)
    6. After IIS had restarted, I clicked the Application Management tab, and then clicked Site collection administrators:
      • I added a user (like Alice or Bob) from the membership provider.

Web.Config Entries

There are a few additions that you have to make to your website's web.config file, as well as the SharePoint Central Administration web.config file for SharePoint 2007:

  • Here's the XML that you need to add to the <system.web> section of your website's web.config; in my example that file would be located at "C:\Inetpub\SharePointSite\wwwroot\web.config":
    <!-- added on 05/31/2011 -->
    <membership defaultProvider="ReadOnlyXmlMembershipProvider">
      <providers>
        <add name="ReadOnlyXmlMembershipProvider"
          type="ReadOnlyXmlMembershipProvider, ReadOnlyXmlMembershipProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=426f62526f636b73"
          description="Read-only XML membership provider"
          xmlFileName="~/App_Data/MyUsers.xml" />
      </providers>
    </membership>
    <roleManager enabled="true" defaultProvider="ReadOnlyXmlRoleProvider">
      <providers>
        <add name="ReadOnlyXmlRoleProvider"
          type="ReadOnlyXmlRoleProvider, ReadOnlyXmlRoleProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=426f62526f636b73"
          description="Read-only XML role provider"
          xmlFileName="~/App_Data/MyUsers.xml" />
      </providers>
    </roleManager>
    <!--/added on 05/31/2011 -->
  • Here's the XML that you need to add to the <system.web> section of your SharePoint Central Administration web.config file; on my server that file is located at "C:\inetpub\wwwroot\wss\VirtualDirectories\6087\web.config":
    <!-- added on 05/31/2011 -->
    <membership defaultProvider="ReadOnlyXmlMembershipProvider">
      <providers>
        <add name="ReadOnlyXmlMembershipProvider"
          type="ReadOnlyXmlMembershipProvider, ReadOnlyXmlMembershipProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=426f62526f636b73"
          description="Read-only XML membership provider"
          xmlFileName="~/App_Data/MyUsers.xml" />
      </providers>
    </membership>
    <!--/added on 05/31/2011 -->

IMPORTANT!!!

SharePoint Central Administration needs to be able to find the MyUsers.xml file, so I created an App_Data folder under physical path of the SharePoint Central Administration website, and I added a symbolic link in that folder that pointed to the physical MyUsers.xml file. Here's how I did that:

  1. I opened a command prompt.
  2. I changed directory to the path where my SharePoint global web.config file was located; for example:
    cd C:\inetpub\wwwroot\wss\VirtualDirectories\6087
  3. I created a symbolic link to the physical path of the XML file; for example:
    mklink MyUsers.xml C:\Inetpub\SharePointSite\wwwroot\App_Data\MyUsers.xml
  4. I closed the command prompt.

Note: I could have copied the XML file, but I preferred to use the symbolic link instead of having to manage two copies of the file.

Optional People Picker Settings

If you were installing a membership provider that can perform lookups, you could add an additional entry to your SharePoint Central Administration web.config file:

<PeoplePickerWildcards>
  <clear />
  <add key="AspNetSqlMembershipProvider" value="%" />
  <!-- added on 05/31/2011 -->
  <add key="ReadOnlyXmlMembershipProvider" value="%" />
  <!--/added on 05/31/2011 -->
</PeoplePickerWildcards>

Problems that I Encountered

Okay - I admit that I everything that I did so far was probably making things harder that they needed to be, but I love a good challenge. ;-]

That being said, I ran into some problems that I thought would be worth mentioning, just in case someone else ran into them.

HTTP 403 Errors

When browsing to my SharePoint website, I received several HTTP 403 errors. I used Process Monitor to troubleshoot the problem, and I discovered that IUSR could not access the "bin" folder in my website. (I'm still not quite sure why it was trying.) To resolve these errors, I used the following steps:

  1. I opened a command prompt.
  2. I changed directory to the SharePoint website's path; for example:
    cd C:\Inetpub\SharePointSite\wwwroot
  3. I changed permissions for the "bin" folder; for example:
    icacls bin /grant IIS_IUSRS:r
  4. I closed the command prompt.

In my situation the problem was for IUSR, but if you are using a different anonymous identity or your application pool is running as a unique identity then it might be a different user. In any case, Process Monitor will let you know who needs permissions.

Later I discovered the following blog post by John Powell:

http://blogs.msdn.com/b/johnwpowell/archive/2008/05/23/sharepoint-intermittent-403-forbidden-errors.aspx

In that blog, John suggests adding the following permissions for the "bin" folder:

icacls bin /grant users:r

I'm not sure if that's necessary, but it's worth pointing out.

HTTP 404.8 Errors

When browsing to my SharePoint website, I received several HTTP 404.8 errors. Those errors mean that the built-in IIS 7 Request Filtering feature was blocking something, so I did the following:

  • I opened my website's web.config file; on my server that file was located at "C:\Inetpub\SharePointSite\wwwroot\web.config":
  • I added the following XML before the closing </configuration> tag:
    <system.webServer>
      <!-- added on 05/31/2011 -->
      <security>
        <requestFiltering>
          <hiddenSegments>
           <clear />
           <add segment="web.config" />
           <add segment="bin" />
           <add segment="App_code" />
           <add segment="App_GlobalResources" />
           <add segment="App_LocalResources" />
           <add segment="App_WebReferences" />
           <add segment="App_Data" />
           <add segment="App_Browsers" />       
          </hiddenSegments>
        </requestFiltering>
      </security>
    <!--/added on 05/31/2011 -->
    </system.webServer>
  • I saved and closed the web.config file.

Note: This removes all of the hidden segments from the global IIS 7 Request Filtering settings, which may be overkill. I have a lot of custom global request filtering settings, and I didn't want to go through each individual setting to see which setting was blocking files that I needed, so I used settings for my website that cleared the inherited request filtering settings and added the default settings.

Annoying Message: "The Web site wants to run the following add-on: 'Name ActiveX Control'"

When browsing to my SharePoint website, the information bar in Internet Explorer kept prompting me with the following message:

The Web site wants to run the following add-on: 'Name ActiveX Control' from 'Microsoft Corporation'. If you trust the Web site and the add-on and want to allow it to run, click here...

This message was highly frustrating, so I did some digging around the Internet and discovered that I could hack the INIT.JS file for SharePoint to suppress this message. Here's how I did that:

  • I opened my server's INIT.JS file; on my server that file was located at "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\1033\INIT.JS".
  • I located the ProcessImn() and ProcessImnMarkers() functions, and I remarked out the contents. Here's what this looked like when I was done:
    function ProcessImn()
    {
    // if (EnsureIMNControl() && IMNControlObj.PresenceEnabled)
    // {
    // imnElems=document.getElementsByName("imnmark");
    // imnElemsCount=imnElems.length;
    // ProcessImnMarkers();
    // }
    }
    function ProcessImnMarkers()
    {
    // for (i=0;i<imnMarkerBatchSize;++i)
    // {
    // if (imnCount==imnElemsCount)
    // return;
    // IMNRC(imnElems[imnCount].sip,imnElems[imnCount]);
    // imnCount++;
    // }
    // setTimeout("ProcessImnMarkers()",imnMarkerBatchDelay);
    }
  • I saved and closed the INIT.JS file.

I should note that this solution is unsupported; and a few months I hacked my INIT.JS file, Microsoft published the following Knowledge Base article with a couple of different methods:

KB 931509: Message in the Information bar in Internet Explorer 7 when you browse to a Windows SharePoint Services 3.0 site or to a SharePoint Server 2007 site: "The Web site wants to run the following add-on: 'Name ActiveX Control'"

That being said, I like my solution better. ;-]


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Cascading Style Sheet (CSS) Color Negatizing Script

The Customer Scenario

I ran into an interesting situation recently - I host a website for a friend of mine, and he was shopping around for a new website template. He found one that he liked, but he didn't like the colors. In fact, he wanted the exact opposite of the colors in the website template, so he asked what I could do about it.

I looked at the website template, and thankfully it was using linked Cascading Style Sheets (CSS) files for all of the color definitions, so I told him that changing the colors would probably be a pretty easy thing to do. However, once I cracked open the CSS files from the website template, I found that they had hundreds of color definitions. Changing every color definition by hand would have taken hours, so I decided to write some Windows Script Host (WSH) code to do the work for me. ;-]

Negatizing a CSS File

With the above scenario in mind, here's the script that I wrote to negatize every color in a CSS file - all that you need to do is replace the paths to the input and output files and run the script to create the negatized version of the input CSS file.

Option Explicit

Const strInputFile = "c:\inetpub\wwwroot\style-dark.css"
Const strOutputFile = "c:\inetpub\wwwroot\style-light.css"

' ------------------------------------------------------------

Dim objFSO
Dim objInputFile
Dim objOutputFile
Dim strInputLine
Dim strLeft, strMid, strRight, strArray
Dim blnFound

' ------------------------------------------------------------

Const strTempRGB = "[|[TMPRGBSTR1NG]|]"
Const strTempHEX = "[|[TMPHEXSTR1NG]|]"

' ------------------------------------------------------------

Set objFSO = CreateObject("scripting.filesystemobject")
Set objInputFile = objFSO.OpenTextFile(strInputFile)
Set objOutputFile = objFSO.CreateTextFile(strOutputFile)

Do While Not objInputFile.AtEndOfStream
    strInputLine = objInputFile.ReadLine
    blnFound = True
    
    Do While blnFound
        If InStr(1,strInputLine,"rgb(",vbTextCompare) Then
            strLeft = Left(strInputLine,InStr(1,strInputLine,"rgb(",vbTextCompare)-1)
            strMid = Mid(strInputLine,InStr(1,strInputLine,"rgb(",vbTextCompare)+4)
            strRight = Mid(strMid,InStr(strMid,")")+1)
            strMid = Left(strMid,InStr(strMid,")")-1)
            strArray  = Split(strMid,",")
            strMid = InvertOctet(CInt(strArray(0))) & _
                "," & InvertOctet(CInt(strArray(1))) & _
                "," & InvertOctet(CInt(strArray(2)))
            strInputLine = strLeft & strTempRGB & "(" & strMid & ")" & strRight
        Else
            blnFound = False
        End If
    Loop
    
    strInputLine = Replace(strInputLine,strTempRGB,"rgb")
    
    blnFound = True

    Do While blnFound
        If InStr(strInputLine,"#") Then
            strLeft = Left(strInputLine,InStr(strInputLine,"#")-1)
            strMid = Mid(strInputLine,InStr(strInputLine,"#")+1)
            If Len(strMid)>6 Then
                strRight = Mid(strMid,7)
                strMid = Left(strMid,6)
            ElseIf Len(strMid)>3 Then
                strRight = Mid(strMid,4)
                strMid = Left(strMid,3)
            Else
                strRight = ""
            End If
            
            If IsHexString(strMid) Then            
                If Len(strMid) = 6 Then
                    strMid = Right("0" & Hex(InvertOctet(CInt("&h" & Left(strMid,2)))),2) & _
                        Right("0" & Hex(InvertOctet(CInt("&h" & Mid(strMid,3,2)))),2) & _
                        Right("0" & Hex(InvertOctet(CInt("&h" & Right(strMid,2)))),2)
                Else
                    strMid = Hex(InvertByte(CInt("&h" & Left(strMid,2)))) & _
                        Hex(InvertByte(CInt("&h" & Mid(strMid,3,2)))) & _
                        Hex(InvertByte(CInt("&h" & Right(strMid,2))))
                End If
            End If

            strInputLine = strLeft & strTempHEX & strMid & strRight
        Else
            blnFound = False
        End If
    Loop
    
    strInputLine = Replace(strInputLine,strTempHEX,"#")
    
    objOutputFile.WriteLine strInputLine
Loop


' ------------------------------------------------------------

Function IsHexString(ByVal tmpString)
    Dim blnHexString, intHexCount, intHexByte
    blnHexString = True
    If Len(tmpString)<>3 and Len(tmpString)<>6 Then
        blnHexString = False
    Else
        tmpString = UCase(tmpString)
        For intHexCount = 1 To Len(tmpString)
            intHexByte = Asc(Mid(tmpString,intHexCount,1))
            If (intHexByte < 48 Or intHexByte > 57) And (intHexByte < 65 Or intHexByte > 70) Then
                blnHexString = False
            End If
        Next
    End If
    IsHexString = blnHexString
End Function

' ------------------------------------------------------------

Function InvertByte(ByVal tmpByte)
    tmpByte = tmpByte And 15
    tmpByte = 15 - tmpByte
    InvertByte = tmpByte
End Function

' ------------------------------------------------------------

Function InvertOctet(ByVal tmpOctet)
    tmpOctet = tmpOctet And 255
    tmpOctet = 255 - tmpOctet
    InvertOctet = tmpOctet
End Function

' ------------------------------------------------------------

Negatizing a SharePoint 2007 Theme

After I wrote the above script, I found myself using it for a bunch of different websites that I manage for other people. One of the websites that I host is based on SharePoint 2007, so I wondered how difficult it would be negatize a SharePoint 2007 theme. As it turns out, it's pretty easy. The following steps will walk you through the steps that are required to create a negatized version of the built-in "Classic" SharePoint 2007 theme.

(NOTE: The steps in this section do not work with SharePoint 2010 or office 14; SharePoint 2010 and Office 14 store their themes in a different format, so these steps will not work.)

  1. Copy the folder:
    "%CommonProgramFiles%\Microsoft Shared\Web Server Extensions\12\TEMPLATE\THEMES\CLASSIC"
    To the following folder:
    "%CommonProgramFiles%\Microsoft Shared\Web Server Extensions\12\TEMPLATE\THEMES\CLASSICNEGATIVE"
  2. Rename the file:
    "%CommonProgramFiles%\Microsoft Shared\Web Server Extensions\12\TEMPLATE\THEMES\CLASSICNEGATIVE\CLASSIC.INF"
    To the following:
    "%CommonProgramFiles%\Microsoft Shared\Web Server Extensions\12\TEMPLATE\THEMES\CLASSICNEGATIVE\CLASSICNEGATIVE.INF"
  3. Open the following file:
    "%CommonProgramFiles%\Microsoft Shared\Web Server Extensions\12\TEMPLATE\THEMES\CLASSIC\CLASSICNEGATIVE.INF"
    • Replace all instances of "Classic" with "Classic Negative".
    • Save and close the INF file.
  4. Open the following file:
    "%CommonProgramFiles%\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\1033\SPTHEMES.XML"
    • Add the following entry to the <SPThemes> collection:
      <Templates>
      <TemplateID>classicnegative</TemplateID>
      <DisplayName>Classic Negative</DisplayName>
      <Description>Classic Negative</Description>
      <Thumbnail>images/thclassic.gif</Thumbnail>
      <Preview>images/thclassic.gif</Preview>
      </Templates>
    • Save and close the XML file.
  5. Edit the color negatizing WSH script from earlier in this blog for each of the following files and run it:
    • theme.css
      • Input File:
        "%CommonProgramFiles%\Microsoft Shared\Web Server Extensions\12\TEMPLATE\THEMES\CLASSIC\theme.css"
      • Output File:
        "%CommonProgramFiles%\Microsoft Shared\Web Server Extensions\12\TEMPLATE\THEMES\CLASSICNEGATIVE\theme.css"
    • mossExtension.css
      • Input File:
        "%CommonProgramFiles%\Microsoft Shared\Web Server Extensions\12\TEMPLATE\THEMES\CLASSIC\mossExtension.css"
      • Output File:
        "%CommonProgramFiles%\Microsoft Shared\Web Server Extensions\12\TEMPLATE\THEMES\CLASSICNEGATIVE\mossExtension.css"

That's all it takes to negate the colors that are defined in the CSS files for a SharePoint 2007 theme. (NOTE: This does not modify the colors of the images in the SharePoint theme; you will need a graphics program to update the colors in the images.)

Closing Thought

Before I receive any comments, I am perfectly aware that "negatize" is not an actual word in the English language, but it seemed appropriate, and new words have to start somewhere. ;-]


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

WordPerfect versus Word

A friend of mine just sent me the following news article, along with the subtitle, "Can we just let this die, geez..."

Novell Antitrust Lawsuit Against Microsoft Revived by Court
Bloomberg Businessweek - May 03, 2011
By Tom Schoenberg

Personally, I find articles like this depressing - not just because they are frivolous lawsuits that do little more than wasting millions of dollars for everyone concerned, but because they send the wrong messages to the business world. Let me explain:

I love quotes that are worded like this: "WordPerfect's share of the word-processing market fell to less than 10 percent in 1996 from almost 50 percent in 1990." This statement is an excerpt from a section in that article which suggests that Microsoft is the bad guy in this situation.

Has anyone ever bothered to consider that whatever happened to WordPerfect occurred because the executive leadership at WordPerfect made a plethora of poor business choices and their applications ceased to be good products? This entire lawsuit reminds me of when Metallica sued Napster over the decline in their album sales - did it ever occur to them (Metallica) that maybe they had passed their prime and perhaps no one wanted to buy their albums anymore?

Here's another question: did anyone else actually try to use WordPerfect for Windows 3.x through Windows 98? Well, I did - because back in my DOS days I was an avid WordPerfect 4.x through 6.x user. So take my word for it, every version of WordPerfect starting from 5.x through 8.x on Windows platforms were simply awful, while at the same time the versions of Word for Windows got better and better.

I can give you several reasons behind this dichotomy, but the primary cause is simple - WordPerfect didn't have a clue how to make a Windows-based product. As the world transitioned from a DOS-based environment to a Windows realm, WordPerfect shipped products that were technologically inferior, way behind schedule, and badly engineered. By the time that the folks at WordPerfect quit wasting money and figured out what they were doing, it was way too late - they owned less than 10% of the market, and the damage was irrevocable.

Here's just one example: instead of leveraging Windows' built-in printing capabilities and investing in better application features and functionality, the people at WordPerfect continued to develop and ship their own printing subsystem, which bypassed the built-in Windows printing features. Even if WordPerfect's alternate printing subsystem had been better, (which I can honestly say from personal experience that it was not), that's not the way that you're supposed to do things in a Windows world, and WordPerfect threw away millions of dollars and countless thousands of man hours on this colossal failure.

Here's another oldie but goodie - WordPerfect bought into the fantasy from the now-defunct Sun Microsystems that Java was the up-and-coming, be-all/end-all of computer languages and the dawn of write-once/run-everywhere software. This was a wonderful theory, and I personally spent some time writing simple applications in Java back in the mid-to-late-1990s because I, too, bought into Sun's hype. (I still wear a Java baseball cap that I got from Sun back in 1996.) But it wasn't long before I, like many others, realized that Java was mostly hype, and writing software in Java was an experience that was more like rewrite-often/debug-everywhere. But I realized my mistake before I had wasted over $400 million on a failed word processing application in Java like WordPerfect did.

But the folks at WordPerfect continued to press on in their self-delusions - all the while falling behind Word, which was now integrating wonderfully with Windows, Microsoft Office, and a host of other applications through technologies like DDE, OLE, and ActiveX. By this point WordPerfect's losses were enormous, then along came Novell, who was already a sinking ship; this was due to the fact that their difficult-to-use and expensive flagship NetWare operating system was taking a serious beating from Windows NT's ease-of-use and significantly reduced barrier-to-entry pricing.

Novell realized that WordPerfect had once been a major cash cow, and I guess they hoped that they could turn around both of these massive sinking ships and get them headed back from the Red Sea into the Black Sea. But Novell's delusions proved to be worse than WordPerfect's, and eventually Novell had to sell WordPerfect to Corel for a pittance just to keep their ship from being dragged under as WordPerfect rocketed toward the bottom in a technology fate that was worse than the demise of the Titanic. And yet, very much like the sinking of the Titanic and the untimely deaths of technology giants like Netscape and Sun Microsystems, WordPerfect's downfall was ultimately caused by a series of gargantuan blunders and the terminal hubris of their leadership, and not by any action on Microsoft's part.

Not that any of this will matter in court - Microsoft will probably still have to shell out a few hundred million dollars in "damages," thereby rewarding former executives at WordPerfect for their incompetence, and reinforcing the message to the business world that just because you're a colossal failure and you ruined the lives of thousands of your loyal employees, that doesn't mean that you shouldn't be able to buy a large mansion and luxury yacht by cashing in on the profits of your successful competitors.


Additional Reading

At the time of this writing, Wikipedia has a great write-up on the history of WordPerfect, including blunt analysis of WordPerfect's many failures. But pages on Wikipedia are subject to change, and they're not always accurate.

With that in mind, you might want to take a look at the book titled Almost Perfect by W. E. Peterson, who had been one of the senior executives at WordPerfect. Sometimes it's nice to have an insider's view of the breakdown and failure.