Wednesday, December 28, 2011

SharePoint 2010 Custom Application Page Affix Ribbon To Top Using CSS

Migrating existing applications into SharePoint can be difficult depending on the JavaScript functionality of the old code. Using the default SharePoint 2010 custom application page, the s4-workspace is a div that is re-sized and scrollable to allow the SharePoint ribbon to display. I don't know why Microsoft felt it necessary to do far more work than necessary to fix a div to the top of the window.

Below is the code I used to fix the scroll bars on the page. This makes the ribbon not fixed and will scroll out of view. This could be enough if you don't use the ribbon in your pages.
body {
    overflow: auto ! important;
}
body.v4master { 
    height:inherit; 
    width:inherit; 
    overflow:visible!important;
}

body #s4-workspace {
   overflow-y:auto !important;
   overflow-x:auto !important;
   height:auto !important;
}

If the ribbon absolutely must be at the top of the page. You can add this bit of code after the above code to properly affix the div to the top of the visible window. My tests showed that this worked for me in IE8, IE 9, and Firefox.

body #s4-ribbonrow {
    left: 0;
    position: fixed;
    top: 0;
    width: 100%;
    z-index: 101;
}
body #s4-workspace {
    padding-top: 44px;
}

That should be it. Not too hard.





Tuesday, December 27, 2011

SharePoint 2010 Custom Application Page jQuery Lightbox Plug-in Fix

I used a fantastic jQuery lightbox plug-in which can be found at http://leandrovieira.com/projects/jquery/lightbox/. There was almost no setup involved. That is, until it meets SharePoint 2010 custom application pages. Since the page doesn't use the body scroll bars and creates faux-scroll bars in the s4-workspace div, it is possible that the image is too large for the visible area and there are no functional scroll bars to view the rest of the picture. I should suffix my last statement with the fact that this probably would have happened with any lightbox plug-in; it just happened that this is the plug-in I used.

I was tasked with finding a fix and below is the result. I only had to add code to the beginning of two functions: _set_interface and _finish. The short of it is that I cache the important styles that I am going to change, then I modify the styles to enable the page scroll bars. When the lightbox is closed, the cached styles are restored.

var htmlbody = $("BODY"),
    bodyMaster = $("body.v4master"),
    bodyWorkspace = $("body #s4-workspace"),
    savedCSS = {
        BodyOverflow: htmlbody.css("overflow"),

        BodyMasterHeight: bodyMaster.css("height"),
        BodyMasterWidth: bodyMaster.css("width"),
        BodyMasterOverflow: bodyMaster.css("overflow"),

        BodyWorkspaceOverflowY: bodyWorkspace.css("overflow-y"),
        BodyWorkspaceOverflowX: bodyWorkspace.css("overflow-x"),
        BodyWorkspaceHeight: bodyWorkspace.css("height")
    };
    settings.SavedCSS = savedCSS;
            
htmlbody.css({ "overflow": "auto" });
bodyMaster.css({ "height": "inherit", "width": "inherit", "overflow": "visible" });
bodyWorkspace.css({ "overflow-y": "auto", "overflow-x": "auto", "height": "auto" });

var htmlbody = $("BODY"),
    bodyMaster = $("body.v4master"),
    bodyWorkspace = $("body #s4-workspace"),
    savedCSS = settings.SavedCSS;

htmlbody.css({ "overflow": savedCSS.BodyOverflow });
bodyMaster.css({ "height": savedCSS.BodyMasterHeight, "width": savedCSS.BodyMasterWidth, "overflow": savedCSS.BodyMasterOverflow });
bodyWorkspace.css({ "overflow-y": savedCSS.BodyWorkspaceOverflowY, "overflow-x": savedCSS.BodyWorkspaceOverflowX, "height": savedCSS.BodyWorkspaceHeight });

SharePoint 2010 Custom Application Page Scroll To Top On Postback

SharePoint 2010 is full of wonderful features that make developers' lives just a bit harder. I ran across an issue where validation was returning a message back to the screen, the page would display the page scrolled to the top and then immediately scroll down to the position to the location of the page prior to posting back. I have had previous run-ins with the s4-workspace, but nothing JavaScript related. I tried several avenues for solutions:

1. Setting the page directive attribute "MaintainScrollPosition" to be false
2. Registering a start up script: $(window).scrollTop(0)
3. Registering a start up script: $("#s4-workspace").scrollTop(0)
4. Attempted to register the the functions via a client script block to add a "pageLoaded" event

The short of it was that none of these worked. I decided to dive in to the HTML source and discovered a "_maintainWorkspaceScrollPosition" hidden field. This looked amazingly like the MaintainScrollPosition functionality, I thought I might be on the right path. The "Workspace" term jumped out at me since the SharePoint custom application page's content is in the s4-workspace; I started to get the feeling that this was a SharePoint feature. After searching all the files for the hidden field, I discovered it was only referenced in the SharePoint JavaScript files. Searching the internet did not yield any solutions on ways to disable the feature. So I generated a function that I would execute to scroll the page to the top.

function scrollToTop() {
    $(window).scrollTop(0);
    $("#s4-workspace").scrollTop(0);
    $("#_maintainWorkspaceScrollPosition").val(0);
}

The function above probably does more than required, but I don't control the Master Page and need to make sure the page can tolerate changes to Master Page style changes.

The server side needs to register a script to execute on the post back. This is a simple line that can be thrown about anywhere.

ScriptManager.RegisterStartupScript(this, this.Page.GetType(), "scrollToTop", "scrollToTop();", true)

Wednesday, September 7, 2011

WCF Service Call with X.509 Certificate Using Powershell

I ran into a situation where I needed to be able to query a WCF service that used an X.509 certificate. I searched the web and was left wanting. So, I turned to powershell. I had previously used powershell to call web services (mostly in Powershell v1), but never hit a WCF service. I found a useful post that took me a little farther, namely it got me to the point where I could script the proxy code generation and compiling the assembly.

I added code to make the binding use the certificate (highlighted line 9 below) and then specified the certificate to use in the client proxy object. My certificate is installed into the Personal store on the local machine (highlighted line 13 below).

Upon testing the code (sans line 10). I received an error "If this is a legitimate remote endpoint, you can fix the problem by explicitly specifying DNS identity '' as the Identity property of EndpointAddress when creating channel proxy." It was trying to match the partial domain name to the fully distinguished domain name. I found that I could fix this limitation by specifying the DNS entry that I was expecting on the endpoint (highlighted line 10).

$proxy = "http://ws.logos.domain:8181/LogosService.svc";
& 'C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\SvcUtil.exe' "$proxy?wsdl"
& 'C:\Windows\Microsoft.NET\Framework\v3.5\csc.exe'  /t:library LogosService.cs /r:"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\System.ServiceModel.dll"

[Reflection.Assembly]::LoadWithPartialName("System.ServiceModel");
[Reflection.Assembly]::LoadFrom("$pwd\LogosService.dll");

$wsHttpBinding = New-Object System.ServiceModel.WSHttpBinding;
$wsHttpBinding.Security.Message.ClientCredentialType = [System.ServiceModel.MessageCredentialType]::Certificate;
$endpoint = New-Object System.ServiceModel.EndpointAddress($memberProxy, [System.ServiceModel.EndpointIdentity]::CreateDNSIdentity("ws.Logos"))

$proxy = New-Object MemberServiceClient($wsHttpBinding, $endpoint)
$proxy.ClientCredentials.ClientCertificate.SetCertificate([System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine, [System.Security.Cryptography.X509Certificates.StoreName]::My, [System.Security.Cryptography.X509Certificates.X509FindType]::FindBySubjectName, "ws.Logos");
$request = New-Object LogosService.ServiceContracts.GetUserInfoRequest;
$request.RequestId = [Guid]::NewGuid();
$request.login = 'domain\test';

$response = $proxy.GetUserInfo($request);

Once I had the $response object, I could get at all the information I needed. Pretty useful.

Thursday, September 1, 2011

JavaScript Format File Size Truncate Decimals

I found a nice bit of JavaScript code which decides what size abbreviation to display for a given number of bytes. For my situation, it was a bit too limited so, I used the awesome bit of logarithmic logic and added the ability to handle null and truncate to a number of decimal places (defaulting to 2 decimals).

function formatSize(bytes, decimals) {
    if (!!!bytes) return 'n/a';
    var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'],
        i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))),
        decMult = Math.pow(10, decimals || 2);
    return (Math.round((bytes / Math.pow(1024, i)) * decMult)) / decMult + ' ' + sizes[i];
}

The bit of logarithmic logic may not be the obvious choice of most developers; I certainly haven't seem that logic anywhere else, but it works. After seeing this and peeking my interest, I wanted to understand how this path of logic was obvious to someone else.

Lets first consider what it would take to achieve the goal using a non-logarithmic solution. It could involve casting the number to a string to find it's length and then doing some calculation on that to produce the index that mapped to the abbreviation. You would have to account for possible fringe cases. Potentially this is easy enough.

For those that despise math, the following may sound similar to Vogon poetry. Plus, I am not a math teacher, so bear with me, I am attempting to think through it logically.

Logarithms are used in many every day situations. The two that come to mind first are decibels (sound volume) and the richter scale (earthquake); both of these use base 10. So each step higher is 10 times the previous value or we can call this a period. In short, it provides a way to turn an exponential equation/graph into a linear equation/graph. It is not important to understand the math, but notice the at 10 maps to 10 and 100 maps to 20.

From: http://www.ndt-ed.org/GeneralResources/decibel/decibel.htm, Note: log is in base 10.
Ratio between Measurement 1 and 2 Equation dB
10 dB = 10 log (10) 10 dB
100 dB = 10 log (100) 20 dB


In this situation we want to determine which period the largest number is in (so to speak). Abstractly, ",...,<3>,<2>,<1>,<0> bytes".

For Example:
25490 would fit in to <1> per the representation above.

Lets start to put the decibels/richter scale and this case together. Decibels uses a base(period) of 10. A base(period) of 10 does not translate usefully to bytes. However, a period of 1024 does mean something.

So we can use Log base 1024 of x bytes (lets write this as log1024(x)) to find out which period it falls in. Javascript has a function (log10) which will evaluate log base 10 of a value. However it doesn't allow you to specify a specific base. I will elide a lot of logic and say that Javascript has a function (log) which is the natural log of a value. See the links below for more information on natural log. To simplify things, we can solve any log base "b" of "a" using only natural logs (math shorthand: ln).

logb(a) = ln(a) / ln(b)
Thus,
log1024(a) = ln(a) / ln(1024)

So we can use Wolfram Alpha to do some examples.

ln(20480) / ln(1024) = 1.432...
http://www.wolframalpha.com/input/?i=ln%2820480
%29%2Fln%281024%29






ln(2048000) / ln(1024) = 2.096...
http://www.wolframalpha.com/input/?i=ln%282048000%29%2Fln%281024%29






If we take the mathematical floor of our natural log formula, we can map this to our list of abbreviations.
...,<3>,<2>,<1>,<0> bytes
[..., GB, MB, KB,bytes]

Mapping the results yields KB for Example 1 and MB for Example 2.

This is a pretty cool line of thought for solving a programming problem mathematically.

Additional information:
The original code that spurred my interest can be found here: http://codeaid.net/javascript/convert-size-in-bytes-to-human-readable-format-%28javascript%29

Wednesday, August 31, 2011

Javascript CAML Where clause OR Builder

It is pretty rare that I run into an occasion where I get to use recursive functions and it is always great. This case is pretty limited, but is perfectly suited for JavaScript. Essentially, I needed to be able to tell if a series of files existed in a SharePoint document library because of an old process where data in the database may contain file names which were not in the SharePoint document library. The root cause of this issue is in the file loading and the database loading processes, however changing these are out of scope.

If the list of possible file names is known we can build a query and have SharePoint return the files that exist. In this case I don't need a reusable function, so I decided that I would take the chance and use an area of JavaScript that I almost never get to utilize: self-referencing anonymous functions or recursive anonymous functions. Below is my implementation, it writes the result of the function to the FireBug/IE Developer Toolbar console.

NOTE: Below there is an issue with the syntax highlighter functionality. If you copy and paste the test below, you must change the "" + field + "" to '" + field + "' and the Type="Text" to Type='Text'
Hopefully there will be a fix to the rendering functionality, but until then, there we are.
files = ["file3.pdf", "file1.pdf", "file2.pdf"];
ors = (function buildOR(field, filelist) {
    var BuildEq = function (field, value) { return "" + value + ""; }
    return (filelist.length == 1) ? BuildEq(field, filelist.shift()) : "" + BuildEq(field, filelist.shift()) + buildOR(field, filelist) + "";
})("FileLeafRef", files)

console.log(ors);
//buildOR("FileLeafRef", files);
Running the example with line 8 uncommented, causes the browser to throw an error. This occurs because the "buildOR" function is an anonymous function. The name "buildOR" is only available inside the function.

Running this example produces the following output (I have formatted it for readability). Note: The FieldRef nodes are self closing, the syntax highlighter appears does not handle these correctly, so I have changed the self closing nodes to accommodate the limitation.

    
        
            
            file3.pdf
        
        
            
                
                file1.pdf
            
            
                
                file2.pdf
            
        
    

Tuesday, August 16, 2011

A Domain Specific Language For SharePoint 2010 Deployment

Visual Studio 2010 and SharePoint 2010 has made great improvements to deployments, but still lacks important features. This method is designed to allow for:
  • Easily moving to a new environment
  • Ease of deployment script enhancements without having to change all files
  • XML driven deployment scripts
  • Reduce need for having to know the SharePoint API
To achive this, we will use a Domain Specific Language (DSL) using XML and powershell as the processor.

We'll start out creating the shared environment variables. These are the only part of the method that is environment specific. I generally have one file that I copy to all environments and only uncomment the environment section.
#
# Environment specific variables for use in powershell scripts
#	Usage:
#		. (Join-Path $currentDirectory SharedEnvironment.ps1);
#
#	Maintenance: 
#		Initial development
#

## VM
$SPRootURL = "http://localhost";
$SPEnvironment = "VM";
$SharePointDeploymentFolder = "C:\SharePointDeploy";
$SharePointSolutionCache = "C:\SharePointDeploy\wsp";
## DEV
#$SPRootURL = "http://SPUrlRoot"; # no ending slash
#$SPEnvironment = "DEV";
#$SharePointDeploymentFolder = "C:\SharePointDeploy";
#$SharePointSolutionCache = "C:\SharePointDeploy\wsp";
The processor is the heart of the method. It takes in the XML document and performs tasks based on the documents contents and what actions are implemented.
param (
	[string] $xmlPath = $(Throw 'Missing: xmlPath'),
	[Switch] $remove
)

[void][System.Reflection.Assembly]::LoadWithPartialName( "Microsoft.SharePoint" );
if((Get-PSSnapin | Where-Object {$_.Name -eq "Microsoft.SharePoint.PowerShell"}) -eq $null) { Add-PSSnapIn "Microsoft.SharePoint.Powershell"; }
$currentDirectory = Split-Path $myInvocation.MyCommand.Path; # $myInvocation.MyCommand.Path;
$WSPCache = ".";
. (Join-Path $currentDirectory MemberCatalogEnvironment.ps1);

# Solution Handling
function ProcessSolution( [System.Xml.XmlNode] $solutionNode )
{
	if ($remove) {
		Retract-Solution $solutionNode;
	} else {
		Install-Solution $solutionNode;
	}
}

function Install-Solution( [System.Xml.XmlNode] $solutionNode )
{
	$curDir = Split-Path -Parent $MyInvocation.ScriptName;
	$fileName = $curDir+"\"+$WSPCache+$solutionNode.File;
	$name = $solutionNode.File;
	
	try
	{
		if (!(test-path $fileName)) {
			$(throw "The file $path does not exist.");
		}
	
		$solution = Get-SPSolution $name -ErrorAction SilentlyContinue
		if ($solution -eq $null) {
			Write-Host "Install Solution: $name";
			#Add solution to SharePoint
			
			Write-Host "Adding solution $name..."
			$solution = Add-SPSolution (get-item $fileName).FullName
			
			#Deploy the solution
			if ($solution.ContainsWebApplicationResource -and $solutionNode.AllWebApplications) {
				Write-Host "Deploying solution $name to $webApplication..."
				$solution | Install-SPSolution -GACDeployment -CASPolicies:$false -AllWebApplications -Confirm:$false
			}
			elseif ($solution.ContainsWebApplicationResource) {
				Write-Host "Deploying solution $name to $webApplication..."
				$solution | Install-SPSolution -GACDeployment -CASPolicies:$false -WebApplication $webApplication -Confirm:$false
			} else {
				Write-Host "Deploying solution $name to the Farm..."
				$solution | Install-SPSolution -GACDeployment -CASPolicies:$false -Confirm:$false
			}
		} else {
			Write-Host "Update Solution: $name";
			try {
				Update-SPSolution –Identity $name –LiteralPath $fileName –GACDeployment -CASPolicies:$false -Confirm:$false
			} catch [Exception] {
				$_ | gm;
				if ($_ -contains "Cannot uninstall the LanguagePack 0 because it is not deployed") {
					Retract-Solution $solutionNode;
					Install-Solution $solutionNode;
				}
				Else {
					throw $_
				}
			}
		}

		WaitForJobToFinish $solutionNode.File
	} catch [Exception] {
		Write-Error $_; 
		log -message $_ -type "Error";

	}
}

function Retract-Solution( [System.Xml.XmlNode] $solutionNode )
{
	Write-Host "Retracting solution $solutionNode.Name...";
	
	# Solution must be uninstalled and removed.
	$curDir = Split-Path -Parent $MyInvocation.ScriptName
	$fileName = $curDir+"\"+$WSPCache+$solutionNode.File
	
	try
	{
		[Microsoft.SharePoint.Administration.SPSolution] $solution = (Get-SPSolution $name -ErrorAction SilentlyContinue)[0];
		if (($solution -ne $null) -and ($solution.Deployed)) {
			Write-Host "Retracting solution."

			if ($solution.ContainsWebApplicationResource -and $solutionNode.AllWebApplications) {
				Write-Host "Retracting solution $name to $webApplication..."
				$solution | Uninstall-SPSolution -AllWebApplications -Confirm:$false
			}
			elseif ($solution.ContainsWebApplicationResource) {
				Write-Host "Retracting solution $name to $webApplication..."
				$solution | Uninstall-SPSolution -WebApplication $webApplication -Confirm:$false
			} else {
				Write-Host "Retracting solution $name to the Farm..."
				$solution | Uninstall-SPSolution -Confirm:$false
			}
			#Uninstall-SPSolution -Identity $solutionNode.File -Confirm:$false
			WaitForJobToFinish $solutionNode.File

			Write-Host "Deleting solution."
			Remove-SPSolution -Identity $solutionNode.File -Confirm:$false
		}elseif (($solution -ne $null) -and ($solution.Deployed))
		{
			Write-Host "Deleting solution."
			Remove-SPSolution -Identity $solutionNode.File -Confirm:$false
		}
	} catch [Exception] {
		Write-Error $_;
		log -message $_ -type "Error";
	}
}

# Feature Handling
function ProcessFeatureActivation( [System.Xml.XmlNode] $featureNode, $retry = 2 )
{
	try
	{
		if (-not $remove) {
			[Microsoft.SharePoint.Administration.SPFeatureDefinition] $feature = Get-SPFeature | ? {$_.DisplayName -eq $featureNode.Name};

			if ($feature -eq $null) {
				Install-SPFeature -path $featureNode.Name;
			}

			if( ($featureNode.Url -ne $null) -and ($featureNode.Url -ne "") )
			{
				$url = $SPRootURL + $featureNode.Url;
				Write-Host 'Enable feature:' $featureNode.Name;
				Enable-SPFeature -identity $featureNode.Name -URL $url;
			}
			else
			{
				Write-Host 'Enable feature:' $featureNode.Name
				Enable-SPFeature -identity $featureNode.Name
			}
		}
	} catch [Exception] {
		Write-Error $_; 
		log -message $_ -type "Error";
	}
}

function ProcessFeatureDeactivation( [System.Xml.XmlNode] $featureNode )
{
	try
	{
		if( ($featureNode.Url -ne $null) -and ($featureNode.Url -ne "") )
		{
			$url = $SPRootURL + $featureNode.Url;
			#stsadm -o deactivatefeature -id $featureNode.Id -url $url
			Write-Host 'Disable feature:' $featureNode.Name;
			Disable-SPFeature -identity $featureNode.Name -confirm:$false -url $url;
		}
		else
		{
			#stsadm -o deactivatefeature -id $featureNode.Id
			Write-Host 'Disable feature:' $featureNode.Name;
			Disable-SPFeature -identity $featureNode.Name -confirm:$false;
		}
	} catch [Exception] {
		Write-Error $_; 
		log -message $_ -type "Error";
	}
}

function ProcessScript( [System.Xml.XmlNode] $scriptNode )
{
	"Executing $($scriptNode.Name)...";
	Invoke-Expression $scriptNode."#text";
}

function ProcessCopyFile( [System.Xml.XmlNode] $copyfileNode )
{
	"Copying $copyfileNode.file to $copyfileNode.destination...";
	$curDir = Split-Path -Parent $MyInvocation.ScriptName
	$fileName = $curDir+"\"+$WSPCache+$copyfileNode.file
	xcopy /Y /E /R $filename $copyfileNode.destination
	if ($LASTEXITCODE -ne 0) {
		Write-Error("Error Copying: " + $filename);
	}
}

function Main 
{
	[string]$xmlName = Split-Path -Path $xmlPath -Leaf
	Start-Transcript "$currentDirectory\$xmlName-log.txt";
	Write-Host "Current Directory: $currentDirectory";
	Write-Host "Config: $xmlPath";
	
	if (test-path $xmlPath) {
		# Found the file in the pwd or via the absolution path
		#"Path Found.";
		$configFileItem = Get-Item $xmlPath;
	} else {
		# Attempt to find the Config XML in the script's directory
		#"Path Not Found.";
		$idx = $inputConfigFile.LastIndexOf("\");
		$configFileItem = Get-Item $(Join-Path $currentDirectory $xmlPath.substring($idx+1,$xmlPath.Length-1-$idx));
	}
	#$configFileItem;
	$configXml = New-Object System.Xml.XmlDocument;
	$configXml.Load( $configFileItem.FullName );
	if ($configXml.SharePointDeploymentConfig.WSPCache) {
		$WSPCache = $configXml.SharePointDeploymentConfig.WSPCache;
	}
	Write-Host "WSP cache location: $WSPCache"; Write-Host("");
	
	#foreach ($taskNode in $configXml.SharePointDeploymentConfig.get_ChildNodes()|?{$_ -ne $null}) { ProcessTask $taskNode;  } 
	$configXml.SharePointDeploymentConfig.get_ChildNodes()|?{$_ -ne $null} | % { ProcessTask $_;  }
	
	Write-Host("");
	
	#[Microsoft.SharePoint.Administration.SPFarm]::Local.solutions | format-table -property name, Deployed, DeployedWebApplications, DeploymentState;
	#"---";[Microsoft.SharePoint.Administration.SPFarm]::Local.solutions | ? { $_.LastOperationResult -ne "DeploymentSucceeded" } | % { $_.Name; $_.LastOperationDetails; $_; };"Done.";
	Stop-Transcript;
}

function ProcessTask( [System.Xml.XmlNode] $taskNode )
{
	#Write-Host("");Write-Host("---")
	# For multiple web.config modifications, sleeping between activations reduces errors.
	if ($taskNode.Sleep -ne $null) {
		Sleep-For $taskNode.Sleep;
	}

	if( $taskNode.get_Name() -eq "Solution" )
	{
		#"Solution"
		#$taskNode;
		ProcessSolution $taskNode
	}
	elseif( $taskNode.get_Name() -eq "SiteCollection" )
	{
		#"SiteCollection"
		ProcessSiteCollection $taskNode
	}
	elseif( $taskNode.get_Name() -eq "FeatureActivate" )
	{
		#"FeatureActive"
		ProcessFeatureActivation $taskNode
	}
	elseif( $taskNode.get_Name() -eq "FeatureDeactivate" )
	{
		#"FeatureDeactive"
		ProcessFeatureDeactivation $taskNode
	}
	elseif( $taskNode.get_Name() -eq "BdcAppDef" )
	{
		"BdcAppDef"
		"Not Implemented"
		ProcessBusinessDataCatalogAppDef $taskNode
	}
	elseif( $taskNode.get_Name() -eq "CopyFile" )
	{
		#"CopyFile"
		ProcessCopyFile $taskNode
	}
	elseif( $taskNode.get_Name() -eq "Script" )
	{
		#"Script"
		ProcessScript $taskNode
	}
}

function WaitForJobToFinish([string]$SolutionFileName)
{ 
    $JobName = "*solution-deployment*$SolutionFileName*"
    $job = Get-SPTimerJob | ?{ $_.Name -like $JobName }
    if ($job -eq $null) 
    {
        Write-Host 'Timer job not found'
    }
    else
    {
        $JobFullName = $job.Name
        Write-Host -NoNewLine "Awaiting job $JobFullName"
        
        while ((Get-SPTimerJob $JobFullName) -ne $null) 
        {
            Write-Host -NoNewLine .
            Start-Sleep -Seconds 2
        }
        Write-Host  "Finished."
    }
}

function Pause ($Message="Press any key to continue...")
{
	Write-Host -NoNewLine $Message
	$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
	Write-Host ""
}

function log {
	param (
		[string] $message = $(throw "Please specify a message."),
		[string] $type = "Information",
		[string] $logName = "Application",
		[string] $sourceName = "Logos Inc Deployment"
	)
	
	$EventLog = Get-EventLog -list | Where-Object {$_.Log -eq $logName}
	$EventLog.MachineName = "."
	$EventLog.Source = $sourceName;
	$EventLog.WriteEntry($message, $type, 1103);
}

. main
After all of that, we can create our simple XML files. The number of XML files depends on the number of deployment options required. There could be 1 XML which deploys the whole environment (see example below) and a couple other XML files to deploy a small subset of functionality.
Some options not used in this example and I will leave it to the reader as an exercise to implement:
  • BCS
  • Site collection restore
  • Copy files elsewhere on the servers - this may be needed if placing files in places where the WSP won't deploy to
Note: Due to an apparent limitation in the syntax highlighter functionality, the XML brush does not handle self closing nodes correctly. For presentation purposes, I have changed the self-closing nodes to have end tags.

	
	
		
	
	
	<Script Name="Remove Session">
		Disable-SPSessionStateService;
	</Script>
	
	<Script Name="Remove Blocked Extension">
		& .\RemoveBlockedExtension -webApplication "http://localhost";
	</Script>
	
	
	
	
	

	<Script Name="Enable Session">
		Enable-SPSessionStateService -DatabaseName "LogosIncSessionState" -SessionTimeout 120
	</Script>
	
	
	
	
	



Now that we have the files needed, we can put them into the directory structure:
C:\SharePointDeploy - Powershell Scripts and XML files
C:\SharePointDeploy\wsp - Cache of WSP files and other deployment files
Usage from the C:\SharePointDeploy directory:
.\SPDeploy.ps1 .\Deploy-Logos-EVERYTHING.xml
.\SPDeploy.ps1 .\Deploy-Logos-EVERYTHING.xml -remove

Saturday, August 6, 2011

Beginning Scriptable Remote SharePoint 2010 Deployment

This post is going to cover how I setup my environment to remotely deploy to SharePoint 2010. It assumes that you have a Powershell script that will perform all of your deployment needs as if you were executing it locally on the server. I will cover my deployment script in a different post.

The following is an example of a non-scripted/able remote deployment. It will attach to the server using CredSSP and prompt for credentials, then it will setup the SharePoint Management Console. Each line must be run separately.

Enter-PSSession -ComputerName devel39.portaldev.doitbestcorp.com -Authentication CredSSP -Credential $([Security.Principal.WindowsIdentity]::GetCurrent().Name);
& ' C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\CONFIG\POWERSHELL\Registration\sharepoint.ps1 '; 
Set-Location C:\Deploy; 
.\Deploy.ps1;
Exit-PSSession;

There are a couple things of note. Enter-SPSession is not useful for scripting as it doesn't send the subsequent commands to the remote session. Start-Transaction, to record the script output, throws an error and continues because it is not supported through PSSession.
This is all well and good if you want to put a lot of effort into deployment.

Powershell provides New-PSSession to open a remote connection. Use the Invoke-Command to send commands to that session.

$remote = New-PSSession -ComputerName name.domain.com;
Invoke-Command -session $remote -scriptblock {
    sl "C:\SharePointDeploy";
    & .\Deploy.ps1 
};
Remove-PSSession $remote;
We can build on this a bit by prompting for credentials and invoking the command as a job. "AsJob" can be loosely equivocated to threads in powershell. There may be many times where the order in which commands end does not matter or you can benefit from an asynchronous programming model which this argument provides.
$remote = New-PSSession -ComputerName name.domain.com -Authentication CredSSP -Credential $([Security.Principal.WindowsIdentity]::GetCurrent().Name); Invoke-Command -session $remote -scriptblock { 
    sl "C:\SharePointDeploy"; 
    & .\Deploy.ps1 
} -AsJob;
Remove-PSSession $remote;

Thursday, July 14, 2011

Setup Windows Remote Management and PSSession

Executing remote commands in Powershell 1 required a good deal of knowledge of powershell and the use of SysInternals' PsExec tool. It had it's drawbacks and inherent insecurites. Perhaps you wanted to remote deploy a SharePoint 2007 package from a TFSBuild script. If the Build Service account doesn't have access to the server or to deploy to SharePoint, psexec needs to have credentials in the command. It works, but not secure. Depending on how much access you have to the build environment, it might be the only option. Using PsExec to deploy packages to SharePoint 2010 throws errors and all attempts thus far have failed. Below is an example adapted from Lee Holmes:
$expression = "C:\SharePointDeploy\Deploy.ps1";
$commandBytes = [System.Text.Encoding]::Unicode.GetBytes($expression)
$encodedCommand = [Convert]::ToBase64String($commandBytes)
psexec /acceptEula /username domain\SPServiceAcct /password s0meP@ssw0rd \\server cmd /c "echo . | powershell -EncodedCommand $encodedCommand"
cmd /c pause
We can use the Windows Remote Management (WinRM) to enable a better, faster remoting experience. Both computers must be set up to allow WinRM. First set up the "remote" server:
Enable-PSRemoting -force
Enable-WSManCredSSP –role Server -force
Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 1000
Before we get the client setup we need to setup the client computer, WinRM cannot be setup when connected to public networks or have network adapters set to public category. Continue after the below script if this doesn't apply. If this is the case, those networks need to be changed to not public. I HIGHLY recommend not being connected to untrusted networks when setting this up.
$nlm = [Activator]::CreateInstance([Type]::GetTypeFromCLSID([Guid]"{DCB00C01-570F-4A9B-8D69-199FDBA5723B}"));
  $connections = $nlm.getnetworkconnections();
  $connections | % {
      Write-Host "Connection " $_.getnetwork().getcategory();
      if ($_.getnetwork().getcategory() -eq 0)
      {
          Write-Host "Setting connection to private.";
          $_.getnetwork().setcategory(1);
      }
  }
It is time to setup the local computer which will be sending the commands to the remote computer. A security decision must be made as to which computers the local computer needs access to. Pass the computer(s) in the arguments.
param (
  $machines = $(throw "machines is required.")  # i.e. "*.domain.com" OR "name1.domain.com, name2.domain.com" sans quotes
)
Enable-PSRemoting -force
Enable-WSManCredSSP –role Client –DelegateComputer $machines -force
Now that the environments are setup, the following can be used to connect to the remote server.
Enter-PSSession -ComputerName name.domain.com
#Run your commands
If you need to specify which user to connect as or use CredSSP, you can use the following (this is needed when you want to get into SharePoint 2010):
Enter-PSSession -ComputerName name.domain.com -Authentication CredSSP -Credential $([Security.Principal.WindowsIdentity]::GetCurrent().Name)
#Run your commands
Finally, to clean up. To exit out of the session:
Exit-PSSession
In a future blog post I will cover using New-SPSession.

I adapted some of the Windows Remote Management (WinRM) parts of Zach Rosenfield's Remote Install of SharePoint (with SPModule) post for the purposes of this post. I probably could have found it in a million different places, but that was the site I found the information on.

Thursday, June 23, 2011

Programmatically Install X509 Certificates And Set Permissions

Using X509 certificates to secure WCF services doesn't take long to setup in a Windows 2008 R2 environment, but is rather user intensive and provides numerous opportunities for error. Below is a script that installs a certificate to the Personal store of the local computer and grants full access to the specified users. This saves time deploying across multiple environments and streamlines the setup for users who don't have experience with certificates.
param
(
	[switch] $Verbose
)

if ($Verbose)
{
    $VerbosePreference = 'Continue'
}

[void][System.Reflection.Assembly]::LoadWithPartialName("System.Security")
$StoreScope = "LocalMachine";
$StoreName = "My";
if (Test-Path "cert:\$StoreScope\$StoreName")
{
	$certfile = Get-Item ".\ServiceX509.pfx";
	$certfile.FullName;

	$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $certfile.FullName,$null;

	try
	{
		$store = New-Object System.Security.Cryptography.X509Certificates.X509Store $StoreName, $StoreScope;
		$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
		$store.Add($cert)
		$store.Close()
		Write-Verbose "Successfully added '$certfile' to 'cert:\$StoreScope\$StoreName'."
		$keyPath = $cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName
		$folderlocation = gc env:ALLUSERSPROFILE 
		$folderlocation = $folderlocation + "\Microsoft\Crypto\RSA\MachineKeys\"  
		$filelocation = $folderlocation + $keyPath 
		("NETWORK SERVICE", "domain\AppPoolSvc") | % { 
			icacls $filelocation /grant "$($_):(F)";
			Write-Verbose "Successfully granted '$_' to '$($cert.FriendlyName)'.";
		}
	}
	catch
	{
		Write-Error ("Error adding '$certfile' to 'cert:\$StoreScope\$StoreName': $_ .") -ErrorAction:Continue
	}
}

Wednesday, April 13, 2011

Powershell Base64 Image Generator For Data URIs

Data URIs are gaining in popularity. There are websites that will generate the URI (like http://www.motobit.com/util/base64-decoder-encoder.asp), but requires the uploading of files to someone else server, corporate policies can make these unusable. I haven't seen one in Powershell, so I made one. The code is pretty basic.

You need to know what MIME type for the file, but they are pretty easy to discover. The most common ones I have used:
  • image/gif
  • image/x-icon
  • image/vnd.microsoft.icon
  • image/png
  • image/jpeg

#####
#
#	Base64.ps1
#		Converts a file to or from base 64
#
#	Useful for generating data protocol image addresses:
#	In CSS:
#		background: #ff url('data:image/gif;base64,<Base64String>')
#
#	In HTML:
#		<img src="data:image/gif;base64,<Base64String>" alt="X">
#
#	Usage (encode):
#		.\base64.ps1 favicon.ico favicon.b64 -encode
#
#	Usage (encode & copy to clipboard):
#		.\base64.ps1 favicon.ico -encode -SetClipboard
#
#	Usage (decode):
#		.\base64.ps1 favicon.b64 favicon.ico -decode
#
#
#####
param (
	[string] $source = $(Throw 'source: required parameter'),
	[string] $destination = "",
	[switch] $encode,
	[switch] $decode,
	[switch] $SetClipboard
)

function Main
{
	$file = Get-Item($source);
	if ($encode)
	{
		$bytes = get-content -encoding byte $file.Fullname
		$result = [System.Convert]::ToBase64String($bytes);
		if ($SetClipboard) {Set-Clipboard $result;}
		if ($destination.length -eq 0)
		{
			return $result;
		}
		else
		{
			set-content -encoding UTF8 -Path ".\$destination" -Value $result;
		}
	}
	elseif ($decode)
	{
		$bytes = get-content -encoding UTF8 $file.Fullname;
		[System.Convert]::FromBase64String($bytes) | set-content -encoding Byte -Path ".\$destination";
	}
	else
	{
		Write-Host("The encode or decode switch is required.");
	}
}

. Main

For more information on Data URIs, refer to RFC2397 or the Wiki page for the Data URI Scheme for examples on how to use them.