Advertisements

Synching AlertSite Devices List with a SharePoint List via AlertSite’s REST API + PowerShell

This one’s a little bit more obscure than usual as I’m not sure how many people use SmartBear Software’s AlertSite service or not. If not, then this should be a good demonstration of how we can use PowerShell to connect to remote REST APIs and then sync the parsed XML response/information to a custom SharePoint List :). On the otherhand, if you don’t care about SharePoint then this demo would also be a good code sample for the .NET crowd as well since AlertSite only has sample code using Perl.

This script assumes that there is a custom SharePoint List created with the following fields:

Title – Single Line of Text (default)
MonitorInterval – Number
BeingMonitored – Yes/No
NotifyOnError – Yes/No
Traceroute – Yes/No
MonitorTimeout – Number

Once you create the fields using the names above, feel free to change the display names. The script will work with the internal field names that are static.

Click here to download the PS1.

PowerShell Script pasted below for search purposes.
You may need to highlight and copy the script below into notepad to be able to read all of the characters that were chopped off by this blog’s layout.

######################################################################################################
# This script will log into AlertSite's REST API and sync the account's devices with a SharePoint List
#
# Author: Henry Ong
#
# AlertSite Documentation: 
# http://help.alertsite.com/AlertSite/RestAPI?skin=clean.nat%2cnat#5_1_Example_of_Report_API
######################################################################################################

############# Start Variables ################
$RESTServer = "https://www.alertsite.com/restapi"
$login = "YourEmailAddress"
$password = "YourPassword"
$postLogin = "<Login><Login>$login</Login><Password>$password</Password></Login>"
$devicesRequest = "<List><APIVersion>1.1</APIVersion><TxnHeader><Request><Login>$login</Login><SessionID>$sessionID</SessionID></Request></TxnHeader><Source></Source></List>"
$SPWebURL = "YourSharePointSiteURL"
$SPListName = "AlertSite Devices"
############# End Variables ##################

[System.Reflection.Assembly]::LoadWithPartialName("System.Net")
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")

##############################################
## Setup user agent and login request
##############################################
$encode = [System.Text.Encoding]::UTF8
$loginBytes = $encode.GetBytes($postLogin)

## Creating and posting a request to AlertSite's REST API Login page
$req = [System.Net.WebRequest]::Create($RESTServer + "/user/login")
$req.Method = "POST"
$req.ContentType = "text/xml"
$req.ContentLength = $loginBytes.Length
$req.UserAgent = "AlertSite REST Client/1.0"

$reqStream = $req.GetRequestStream()
$reqStream.Write($loginBytes, 0, $loginBytes.Length)
$reqStream.Close()

## Working with the response sent back from AlertSite
$resp = $req.GetResponse()
$cookie = $resp.Headers["Set-Cookie"]

$responseStream = $resp.GetResponseStream()
$respReader = (New-Object System.IO.StreamReader($responseStream))
$XMLResponse = $respReader.ReadToEnd()
$responseStream.Close()

# Save Session ID and Customer Object ID for subsequent API calls
$XMLEncoded = [xml]$XMLResponse

$sessionID = $XMLEncoded.Response.SessionID
$objCust = $XMLEncoded.Response.ObjCust
$resp.Close()

##############################################
## Getting the list of devices from AlertSite
##############################################

$encode = [System.Text.Encoding]::UTF8
$listDeviceBytes = $encode.GetBytes($devicesRequest)

$req = [System.Net.WebRequest]::Create($RESTServer + "/devices/list")
$req.Method = "POST"
$req.ContentType = "text/xml"
$req.ContentLength = $listDeviceBytes.Length
$req.UserAgent = "AlertSite REST Client/1.0"
$req.Headers.Add("Cookie", $cookie)

$reqStream = $req.GetRequestStream()
$reqStream.Write($listDeviceBytes, 0, $listDeviceBytes.Length)
$reqStream.Close()

$resp = $req.GetResponse()

$responseStream = $resp.GetResponseStream()
$respReader = (New-Object System.IO.StreamReader($responseStream))
$XMLResponse = $respReader.ReadToEnd()
$responseStream.Close()

$XMLEncoded = [xml]$XMLResponse
$resp.Close()

###########################################
# Update SharePoint List
###########################################

$site = New-Object Microsoft.SharePoint.SPSite($SPWebURL)
$web = $site.OpenWeb()
$deviceList = $web.Lists[$SPListName]

foreach($device in $XMLEncoded.Response.TxnList.Txn)
{
    $deviceName = InjectSpaces $device.TxnName
    $interval = $device.TxnDetail.GetAttribute("Interval")
    $monitored = ReturnBoolFromLetter $device.TxnDetail.GetAttribute("Monitor")
    $notify = ReturnBoolFromLetter $device.TxnDetail.GetAttribute("Notify")
    $traceRoute = ReturnBoolFromLetter $device.TxnDetail.GetAttribute("TraceError")
    $timeout = $device.TxnDetail.GetAttribute("TimeOut")

    $listItem = FindDeviceInSharePoint $deviceName

	if($listItem -eq $null)
    {        
        CreateNewListItem $deviceList $deviceName $interval $monitored $notify $traceRoute $timeout
    }

    elseif($listItem -ne $null)
    {
        UpdateListItem $listItem $deviceName $interval $monitored $notify $traceRoute $timeout
    }

}

$spweb.dispose()
$spsite.dispose()

####################################################################################################
## This function will replace the "%20" characters in the string with spaces.
####################################################################################################
function InjectSpaces($string)
{
	$newString = $string.replace("%20", " ")
	return $newString
}

####################################################################################################
## This function will return the SharePoint List Item that represents the device if it exists.
####################################################################################################
function FindDeviceInSharePoint($device)
{
	$listQuery = New-Object Microsoft.SharePoint.SPQuery
	$listQuery.Query = "<Where><Eq><FieldRef Name='Title' /><Value Type='Text'>" + $device + "</Value></Eq></Where>"
	$itemCollection = $deviceList.GetItems($listQuery)
	if($itemCollection.count -eq 1)
	{
		return $itemCollection[0]
	}
	else
	{
		return $null 
	}
}

####################################################################################################
## This function will create a new SharePoint List Item to represent the AlertSite Device if 
## it doesn't exist.
####################################################################################################
function CreateNewListItem($spList, $deviceName, $monitorInterval, $beingMonitored, $notifyOnError, $traceRouteOnError, $monitorTimeout)
{
    $listItem = $spList.Items.Add()
    $listItem["Title"] = $deviceName
    $listItem["MonitorInterval"] = $monitorInterval
    $listItem["BeingMonitored"] = $beingMonitored
    $listItem["NotifyOnError"] = $notifyOnError
    $listItem["Traceroute"] = $traceRouteOnError
    $listItem["MonitorTimeout"] = $monitorTimeout
    $listItem.update()
}

####################################################################################################
## This function will update an existing SharePoint List Item that represents an AlertSite Device.
####################################################################################################
function UpdateListItem($listItem, $deviceName, $monitorInterval, $beingMonitored, $notifyOnError, $traceRouteOnError, $monitorTimeout)
{
    $listItem["MonitorInterval"] = $monitorInterval
    $listItem["BeingMonitored"] = $beingMonitored
    $listItem["NotifyOnError"] = $notifyOnError
    $listItem["Traceroute"] = $traceRouteOnError
    $listItem["MonitorTimeout"] = $monitorTimeout
    $listItem.update()
}
####################################################################################################
## This function will convert the AlertSite attributes for SharePoint consumption.
####################################################################################################
function ReturnBoolFromLetter($letter)
{
    if($letter -eq "y")
    {
        return $true
    }

    elseif($letter -eq "n")
    {
        return $false
    }
}
Advertisements

SharePoint Web Page/Valid Site Monitoring with Email Alerts PowerShell Script

Two scripts in two days? Am I out of my mind?! Yes, no, yes, maybe… Yesterday’s SharePoint Diagnostic Log Monitor + Email Alert PowerShell Script worked so well that I was getting bombarded with too many emails! Yes, they were all critical as far as SharePoint was concerned and I was able to resolve a great majority of them. But some of them just seemed to be inherent to SharePoint and didn’t really affect the end-user experience therefore the criticality of these alerts became less of a priority.

So today I went about bootlegging a different type of monitoring system that would be more indicative of an end user’s experience with the site. With this script, I wanted to connect to the SharePoint sites and see if I get a valid response or valid content back from the server. If I can’t download the page with the System.Net.WebClient.DownloadString() method, then it’ll throw an exception and that’s how I’ll know whether the site is accessible by an end-user or not. If it’s not accessible, then an email will be generated and sent to me. Pretty cool eh?

This is extra useful when used in an environment with load balanced front ends and you want to know exactly which server is having issues. If you want to use this script in this manner, then make sure you create the scheduled tasks for each server.

Sample Email Output:

URL: https://url1
Server: ALW01

Exception: System.Net.WebException: The remote server returned an error: (500) Internal Server Error. at System.Net.WebClient.DownloadDataInternal(Uri address, WebRequest& request) at System.Net.WebClient.DownloadString(Uri address) at DownloadString(Object , Object[] ) at System.Management.Automation.DotNetAdapter.AuxiliaryMethodInvoke(Object target, Object[] arguments, MethodInformation methodInformation, Object[] originalArguments)

===================================

URL: https://url2
Server: ALW02

Exception: System.Net.WebException: The remote server returned an error: (500) Internal Server Error. at System.Net.WebClient.DownloadDataInternal(Uri address, WebRequest& request) at System.Net.WebClient.DownloadString(Uri address) at DownloadString(Object , Object[] ) at System.Management.Automation.DotNetAdapter.AuxiliaryMethodInvoke(Object target, Object[] arguments, MethodInformation methodInformation, Object[] originalArguments)

===================================

You may need to highlight and copy the script below into notepad to be able to read all of the characters that were chopped off by this blog’s layout.

############# Start Variables ################
$urls = @("https://url1", "https://url2")
$emailFrom = "SharePoint.Automation@email.com"
$emailTo = @("email1","email2")
$subject = "SharePoint Down!"
$smtpserver = "SMTPServer"
$server = "ALW01"
# When used in a load-balanced environment where
# each server has host entries to itself, this can help you
# identify which server is having issues.
############# End Variables ##################

[System.Reflection.Assembly]::LoadWithPartialName("System.Net")
$wc = New-Object System.Net.WebClient 
$wc.UseDefaultCredentials = $true
$body = ""

foreach($url in $urls)
{  
    try  
    {   
        $page = $wc.DownloadString($url);   
        if($page.Contains("An unexpected error has occurred.") -or $page.Contains("Cannot connect to the configuration database"))   
        {    
            $body += "<b>URL:</b> " + $url + "<br><br>"    
            $body += "<b>Server:</b> " + $server + "<br><br>"    
            $body += "<b>Exception:</b> " + "Getting a nasty error. Please help me." + "<br><br>"    
            $body += "===================================<br><br>"    
        }  
    }  

    catch [Exception]  
    {   
        $body += "<b>URL:</b> " + $url + "<br><br>"   
        $body += "<b>Server:</b> " + $server + "<br><br>"   
        $body += "<b>Exception:</b> " + $_.Exception + "<br><br>"   
        $body += "===================================<br><br>"   
    }
}

if($body -ne "")
{  
    Send-MailMessage -To $emailTo -Subject $subject -Body $body -SmtpServer $smtpserver -From $emailFrom -BodyAsHtml
}

SharePoint Diagnostic Log Monitor + Email Alert PowerShell Script

Looking for a bootleg SharePoint monitoring and alert system? Well, you’ve come to the right place! I wrote the following PowerShell script to be run as a scheduled task against SharePoint’s Diagnostic Logs. It will not only find the latest log file to analyze, but it will slice and dice through that current log file for the most critical “Critical” events. Once it finds those Critical events that are usually indicative of something really wrong with your environment, it’ll package up all of the messages in an HTML email and send it your way.

You may need to highlight and copy the script below into notepad to be able to read all of the characters that were chopped off by this blog’s layout.

############# Start Variables ################ 
$logDirectory = "D:\Logs\Diagnostic Logs\*.log" 
$emailFrom = "FromEmailAddress" 
$emailTo = @("Email1","Email2") 
$subject = "SharePoint Diagnostics Critical Alert" 
$smtpserver = "EmailServer" 
############# End Variables ##################
$latestLogFile = get-childitem $logDirectory | sort LastWriteTime -desc | select -first 1
$criticalItems = Select-String $latestLogFile.FullName -Pattern "Critical"
if($criticalItems -ne $null) 
{  
     $body = ""  
     foreach($criticalItem in $criticalItems)  
     {   
         $body += "<b>Error:</b> " + $criticalItem.Line + "<br><br>"   
         $body += "<b>Line Number:</b> " + $criticalItem.LineNumber + "<br><br>"   
         $body += "<b>File Path:</b> " + $criticalItem.Path + "<br><br>"   
         $body += "===================================<br><br>"  
     }    

     Send-MailMessage -To $emailTo -Subject $subject -Body $body -SmtpServer $smtpserver -From $emailFrom -BodyAsHtml 
}

 

Adding SharePoint Social Rating Stars to the Homepage of SharePoint Blog Sites

The goal of this adventure was to leverage the SharePoint blog site template and SharePoint Social Rating capabilities to construct an ideation type of site.

But alas, this was one of those things that took me an ungodly amount of time to try and figure out a solution for… hopefully this post will be able to help others solve the same problem. Yes, there are lots of blog posts out there that show how we can add the rating capabilities to individual blog post entries, but I couldn’t find one that described the process for adding the rating stars to the homepage of the blog site template’s summary view web part for each of the posts. With some help with the MSDN SharePoint Forums and a fellow by the name of Raghavendra Shanbhag, we figured it out with the steps outlined below.

1. Assuming that your blog site has been created, turn on ratings for your Posts Document Library as described here.

2. Download this custom blog.xsl file and upload it to a Style Library or any other Document Library of your choice. Note at around line 121 is where we added the AverageRating field to the output which is the only customization that was performed on the original XSL.

3. Open the blog site homepage and edit it in SharePoint Designer. Here, you’ll want to click on the Posts web part and add the Rating (0-5) column for the field to be displayed in the view.

4. While you’re in SharePoint Designer, you can also include a link to the custom XSL that you had uploaded in Step 2.

5. Now go ahead and save all the changes that you’ve made in SharePoint Designer and then head over to your blog site in the browser. You should now see the rating stars below the title of each of your posts but you’ll find that they are not clickable/interactive. What you’ll want to do here is to edit the homepage of that blog site and then add another instance of the Posts list to one of the web part zones. After you add it, change the view on that web part to the “All Posts” view which should contain the rating stars. By adding this web part, it registers the necessary javascripts and CSS for the rating stars to properly function. But we don’t want to have a duplicate set of posts on our homepage right? What we can do here is mark the 2nd Posts web part as hidden!

And with that, we have now transformed a SharePoint blog site into an ideation site 🙂

SharePoint 2010 Intermittent Search Errors & Web Services Round Robin Service Load Balancer Event: EndpointFailure

Environment:

SharePoint 2010 Enterprise
3 WFE’s each with Query Roles
2 App Servers with 1 Dedicated Indexer

Error in Event Viewer:

Event ID: 8313

SharePoint Web Services Round Robin Service Load Balancer Event: EndpointFailure

Process Name: w3wp

Process ID: 5948

AppDomain Name: /LM/W3SVC/303076968/ROOT-1-129634027688317155

AppDomain ID: 2

Service Application Uri: urn:schemas-microsoft-com:sharepoint:service:767f7d712e064db3bb8945d9d42d0e12#authority=urn:uuid:f7625c7a33924a9981c0206f8bf0054a&authority=https://applicationserver:32844/Topology/topology.svc

Active Endpoints: 3

Failed Endpoints:1

Affected Endpoint: http://applicationserver:32843/767f7d712e064db3bb8945d9d42d0e12/SearchService.svc

Error in Diagnostic Logs:

Internal server error exception: System.ServiceModel.ServerTooBusyException: The HTTP service located at http://applicationserver:32843/767f7d712e064db3bb8945d9d42d0e12/SearchService.svc is too busy.  —> System.Net.WebException: The remote server returned an error: (503) Server Unavailable.     at System.Net.HttpWebRequest.GetResponse()     at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelRequest.WaitForReply(TimeSpan timeout)

Troubleshooting Steps:

I tried to access the web service that SharePoint was trying to get to via the browser and noticed that it was reporting a 503 error as reported in the Diagnostic Logs. Then I hopped onto IIS on the affected servers and looked at the SharePoint Web Services web application (IIS) in search for a corresponding Service Application GUID that matched. The application didn’t exist on either of the application servers so that would cause the EndpointFailure as reported in the Event Viewer logs. I then double checked the Services on Server page for the application servers to make sure that the search services were turned on as having these turned on would provision the web services. They were all started which threw a little confusion my way but with some hints from this forum post, I went ahead and stopped/started the Search Query and Site Settings Service. After a little while, assuming that some timer jobs had to run, the Service Application with the missing GUID showed up in IIS and the error stopped appearing in the Event logs.

Summarized Solution:

1. Go to your Services on Server page in Central Administration.

2. Stop and Start the Search Query and Site Settings Service on each affected server as reported in the Event Viewer or SharePoint Diagnostic Logs.

SharePoint Governance for Document Library Versioning

SharePoint Governance… a topic beaten to death right? Luckily for you that’s not what I’m here to talk about today. Today, I’ll be talking about how we can run reports that can actually help us analyze and potentially enforce governance rules upon your awesome user community that unwittingly turned on versioning for all of their Document Libraries and conveniently forgot to include a version limit number while they continue to upload 50 MB PowerPoint slide decks with the same file name over and over again, creating a bajillion versions that go back 5 years… phew. Yes that was meant to be a run-on sentence. But it’s quite a common problem isn’t it? So how do we deal with this…

Since I work in an environment with too many Site Collections and Document Libraries to troll through manually, I’ve written a handy dandy PowerShell script to do all the work for me. This script will give me a report with the following information:

My Report Output Header

Description

ListTitle The title of the SharePoint list with BaseType = DocumentLibrary
ListURL The server relative URL of the SharePoint list. This script assumes you’re working with one Site Collection at a time.
ListAuthor The person that created the list.
ListItemCount The number of items in the list.
ListVersioningMajor Whether or not major versioning is enabled for this list.
ListVersioningMinor Whether or not minor versioning is enabled for this list.
MajorVersionLimit The number of major versions to keep if limited.
MinorVersionLimit The number of minor versions to keep if limited.
ListItemTitle The name of the list item/document being reported on.
ListItemAuthor The last person that modified the list item/document being reported on.
ListItemLastMod The last modified time stamp for the list item/document being reported on.
ListItemVersions The number of versions (major and minor) associated with the list item/document being reported on.
ListItemFileSize The file size for the current version of the list item/document being reported on.
ListItemFileSizeCorpus The aggregated storage space being used by all versions. Note: This is not an exact number but a simple estimate of the size of the current version multiplied by the number of versions associated with the list item/document being reported on.

The PowerShell Script (Download Link)

# This script will generate a report of lists of type "DocumentLibrary" 
# and will give details on all of the files and their versioning attributes.
#
# Author: Henry Ong
######################## Start Variables ########################
$siteURL = "http://SiteCollection" #URL to the Site Collection you want to analyze.
$filePath = "D:\PowerShellScripts\AllVersionedLibraries.csv"
######################## End Variables ########################
if(Test-Path $filePath)
{
 Remove-Item $filePath
}
Clear-Host
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
# Creates an object that represents a SharePoint List
function CreateNewListObject
{
 $listObject = New-Object system.Object
 $listObject | Add-Member -type NoteProperty -Name ListTitle -Value $list.Title
 $listObject | Add-Member -type NoteProperty -Name ListURL -Value $list.DefaultViewURL
 $listObject | Add-Member -type NoteProperty -Name ListAuthor -Value $list.Author
 $listObject | Add-Member -type NoteProperty -Name ListItemCount -Value $list.ItemCount
 $listObject | Add-Member -type NoteProperty -Name ListVersioningMajor -Value $list.EnableVersioning
 $listObject | Add-Member -type NoteProperty -Name ListVersioningMinor -Value $list.EnableMinorVersions
 $listObject | Add-Member -type NoteProperty -Name MajorVersionLimit -Value $list.MajorVersionLimit
 $listObject | Add-Member -type NoteProperty -Name MinorVersionLimit -Value $list.MajorWithMinorVersionsLimit

 return $listObject
}
# Creates an object that represents a SharePoint List Item
function CreateNewListItemObject
{
 $listItemObject = New-Object system.Object
 $listItemObject | Add-Member -type NoteProperty -Name ListTitle -Value $list.Title
 $listItemObject | Add-Member -type NoteProperty -Name ListURL -Value $list.DefaultViewURL
 $listItemObject | Add-Member -type NoteProperty -Name ListAuthor -Value $list.Author
 $listItemObject | Add-Member -type NoteProperty -Name ListItemCount -Value $list.ItemCount
 $listItemObject | Add-Member -type NoteProperty -Name ListVersioningMajor -Value $list.EnableVersioning
 $listItemObject | Add-Member -type NoteProperty -Name ListVersioningMinor -Value $list.EnableMinorVersions
 $listItemObject | Add-Member -type NoteProperty -Name MajorVersionLimit -Value $list.MajorVersionLimit
 $listItemObject | Add-Member -type NoteProperty -Name MinorVersionLimit -Value $list.MajorWithMinorVersionsLimit 
 $listItemObject | Add-Member -type NoteProperty -Name ListItemTitle -Value $item.Name
 $listItemObject | Add-Member -type NoteProperty -Name ListItemAuthor -Value $item["Modified By"]
 $listItemObject | Add-Member -type NoteProperty -Name ListItemLastMod -Value $item["Modified"]
 $listItemObject | Add-Member -type NoteProperty -Name ListItemVersions -Value $versions
 $listItemObject | Add-Member -type NoteProperty -Name ListItemFileSize -Value $fileSize
 $listItemObject | Add-Member -type NoteProperty -Name ListItemFileSizeCorpus -Value $fileSizeTotal

 return $listItemObject
}
$site = new-object microsoft.sharepoint.spsite($siteURL)
$customListObjects =@()
$allWebs
= $site.AllWebs

foreach ($web in $allWebs)
{
 foreach ($list in $web.Lists)
 {
  if($list.BaseType -eq "DocumentLibrary")
  {

   $customListObject = CreateNewListObject
   $customListObjects += $customListObject 

   if($customListObject.ListVersioningMajor -eq $true -or $customListObject.ListVersioningMinor -eq $true)
   {
    foreach($item in $list.items)
    {
     $versions = $item.versions.count
     $itemID = $item.UniqueId
     $file = $web.GetFile($itemID)
     $fileSize = $file.TotalLength/1000000
     $fileSizeTotal = $fileSize*$versions

     $customListItemObject = CreateNewListItemObject
     $customListObjects += $customListItemObject
    }
   }

   Write-Host $list.Title
   Write-Host $web.title
  }
 }

 $web.Dispose()
}
$site.dispose()
# Exporting the data to a CSV file
$customListObjects | Select-Object ListTitle,ListURL,ListAuthor,ListItemCount,ListVersioningMajor,ListVersioningMinor,MajorVersionLimit,MinorVersionLimit,ListItemTitle,ListItemAuthor,ListItemLastMod,ListItemVersions,ListItemFileSize,ListItemFileSizeCorpus | Export-Csv $filePath
write-host "Done"

Analyzing the Report

So once the report is run, I typically delete the first line with junk data that says “#TYPE Selected.System.Object” (anyone know how to not have that outputted?) and then convert everything into a table. That’s done by selecting all of the data (CTRL-A) and then going to the Insert tab of the Excel Ribbon and then selecting Table and include headers. Now with this information in a sortable/filterable format, I can go through any of the columns to better determine which Document Libraries or documents are potential problems. For example:

1. I’ll first filter on “ListVersioningMajor=TRUE” to figure out which ones have versioning turned on.

2. Then I can filter on “MajorVersionLimit” to figure out which ones don’t have a limit in place.

3. And then I’ll sort the “ListItemVersions” column to figure out which documents have a ton of versions.

Now that’s just one way to look at this report to figure out next steps, I’m sure you’ll find other pivots on this data that you’ll find useful.


Key Take Aways

Just having a governance plan in place is not good enough. Your governance plan needs to be actionable and enforceable. Using scripts like this to provide both proactive and reactive reporting can help you and your Site Owners by providing data-driven firepower to effectively manage your SharePoint environments. What am I doing with this data? Depending on the scenario I can:

1. Send the report to the Site Owner to mull over and figure out where and how much they want to limit the versioning or…

2. I can say hey, it looks like you’re not using versioning at all. Mind if I turn it off? And that’s a post for next time – How to automatically change the versioning settings for all Document Libraries in a Site Collection.

Basic Page Document Libraries upgraded to SharePoint 2010 and an error message: “Cannot save your changes”

This one was quite an anomally so I thought I’d share…

Scenario:

I have a SharePoint sub-site with multiple Document Libraries created with the Basic Page Document Template. These libraries were upgraded from MOSS 2007 to SharePoint 2010. Users with Full Control were able to modify/edit the pages just fine but users with Contributor rights were not able to save changes made to existing content pages. When Contributors tried to save changes made with the WYSIWYG editor, which seems to be a variation of the Content Editor Web Part, they get an browser popup error message that says “Cannot save your changes“.

Troubleshooting Steps:

I went up, down and all around all of the permission configurations on the site, library, parents, etc. but couldn’t see why a user with Contributor rights was not able to save their changes. Also of note: the content rendered ok in IE7 but didn’t render at all for the user in Firefox. I finally ended up deleting the page and tried to recreate it with the affected user’s session using Firefox. That’s when I was given a much better clue. The Content Editor Web Part on the page returned: “A Web Part or Web Form Control on this Page cannot be displayed or imported. You don’t have Add and Customize Pages permissions required to perform this action.”

Solution:

With that clue, I trekked up to the root site and modified the permission levels for Contributors. Site Actions > Site Permissions > Permission Levels (in the ribbon) > Clicked on Contribute and then checked the “Add and Customize Pages” option. And that did it!