Thursday, August 11, 2016

Powershell to edit a SharePoint Web Application's web.config file

You really want to do this instead of directly editing the web.config file.  If you change the web.config directly, SharePoint won't know it's actually changed.  During the next patch or Config Wizard, SharePoint will overwrite the web.config with what it thinks should be there, which is bad.

For our purposes, we store the config changes in a csv file that Powershell will run.  That config file holds things like cache timeout settings for some of our custom apps, a custom branding url, and a switch for debug mode to be enabled for specific apps.

That config file can contain anything you might want, but it will need three columns.  They are:  Name, Owner, and Value

Ours looks like this:


Name Owner Value
app.DebugMode itsp FALSE
app.BrandingUrl itsp http://somesite.com/sites/common
app.Cache.CacheTimeout itsp 1
app.Cache.IsCacheEnabled itsp FALSE
app.Lists.EventsList itsp Events Calendar
app.Lists.ContactsList itsp Contacts
app.Lists.LinksList itsp Local Links
app.Lists.AssetsList itsp Site Assets


The Powershell is:

# Change this variable to the target Web App
$webAppUrl = "http://somewebapp.com"
$CSVLocation = "F:\Temp\ConfigMods.csv"


# Add Snapin if it is not already loaded
if ((Get-PSSnapin | ? {$_.Name -eq "Microsoft.SharePoint.PowerShell"}) -eq $null)
{
    Add-PSSnapin "Microsoft.SharePoint.PowerShell"
}

# Import configuration
$mods = Import-CSV $CSVLocation

$webApp = Get-SPWebApplication $webAppUrl

# Get the mods that need to be checked
$ownerstodelete = $mods | Group-Object owner | Select-Object Name

# For each mod
foreach($owner in $ownerstodelete)
{
    # Create an array
    $modstodelete = @()

    # For each current web mod
    foreach($mod in $webApp.WebConfigModifications)
    {
        # If mod is marked
        if($mod.Owner -eq $owner.Name)
        {
            # Add to the array
            $modstodelete += $mod
        }
    }

    Write-Host "Removing $($modstodelete.Count) web.config modifications from $($webAppUrl)" -ForegroundColor Yellow

    # Foreach mode to be deleted
    foreach($delmod in $modstodelete)
    {
        # Delete the mod
        $webApp.WebConfigModifications.Remove($delmod) > $null
        Write-Host " + Deleted mod $($delmod.Value)" -ForegroundColor Red
    }
}

# Reset sequence
$i = 0;

Write-Host ""
Write-Host "Adding $($mods.Count) web.config modifications to $($webAppUrl)" -ForegroundColor Yellow

# For each mod to add
foreach($modEntry in $mods)
{
    # Create the mod value
    $modValue = [system.string]::format(“<add key=""{0}"" value=""{1}"" />", $modEntry.Name, $modEntry.Value)

    # Create the mod object
    $mod = New-Object Microsoft.SharePoint.Administration.SPWebConfigModification
    $mod.Path = "configuration/appSettings"
    $mod.Name = [string]::Format("add[@key='{0}']", $modEntry.Name)
    $mod.Sequence = $i++
    $mod.Owner = $modEntry.Owner
    $mod.Type = 0
    $mod.Value = $modValue

    # Add the mod to the web app
    $webApp.WebConfigModifications.Add($mod)
    Write-Host " + Added mod $($modValue)" -ForegroundColor Cyan
}

# Update the web app
$webApp.Update()

# Apply web config changes
$webApp.WebService.ApplyWebConfigModifications()

# Run all Admin timer jobs
net stop SPAdminV4 > $null
Start-SPAdminJob
net start SPAdminV4 > $null

Write-Host ""
Write-Host "Web.config changes have been applied successfully" -ForegroundColor Green

Powershell to rename a Site Collection or change the Site Collection's managed path

This one will actually back up the named Site Collection, then restore it using the new managed path.  One thing most people don't mention is this won't necessarily put the Site Collection back in the same database it came from - so keep an eye on your databases in the Manage Content Databases area of Central Admin.  You want to be sure the Site Collection you're moving is in a content database that has enough Site Collections available before it hits a warning.  You also want to make sure you don't have another content db out there that has a much higher number of available slots.

SharePoint is lazy - and it'll go where the most room is.  Keep an eye on that.

Anyway, here's the script.

***

Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue



#Get the Source Site Collection URL

$sourceURL = Read-Host “Enter the Source URL”

 

#Get the Target Site Collection URL

$targetURL = Read-Host “Enter the Destination URL”

 

#Location for the backup file

$backupPath = Read-Host “Where do you want the backup stored”



Try

{

  #Set the Error Action

  $ErrorActionPreference = "Stop"



 Write-Host "Backing up the Source Site Collection..."-ForegroundColor DarkGreen

 Backup-SPSite $sourceURL -Path $backupPath -force

 Write-Host "Backup Completed!`n"



 #Delete source Site Collection

 Write-Host "Deleting the Source Site Collection..."

 Remove-SPSite -Identity $sourceURL -Confirm:$false

 Write-Host "Source Site Deleted!`n"



 #Restore Site Collection to new URL

 Write-Host "Restoring to Target Site Collection..."

 Restore-SPSite $targetURL -Path $backupPath -Confirm:$false

 Write-Host "Site Restored to Target!`n"



 #Remove backup files

 Remove-Item $backupPath

}

catch

{

 Write-Host "Operation Failed. Find the Error Message below:" -ForegroundColor Red

 Write-Host $_.Exception.Message -ForegroundColor Red

}

finally

{

 #Reset the Error Action to Default

 $ErrorActionPreference = "Continue"

}



write-host "Process Completed!"

Tuesday, August 2, 2016

Forcing PDFs to open in the client in SharePoint 2013

So you want to have total control over how a PDF opens in SharePoint?  You're probably like me in that your implementation has Office Web Apps (OWA), and your organization is using Firefox, IE, maybe Safari and Chrome.

All these browsers have different ways of handling PDFs, with OWA having another method on top of all that.  But if you want to take advantage of signing / active forms in PDFs, you'll need to force SharePoint to use the Adobe client to open the files.

Luckily, there's an easy way to do this.  Javascript to the rescue.

First, take the Javascript below, save it as pdffix.js, and put it in the C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\TEMPLATE\LAYOUTS directory of each Web Front End server on your farm:

(function () {

    if (typeof SPClientTemplates === 'undefined')

        return;



    var PdfCtx = {};

    PdfCtx.Templates = {};

    PdfCtx.Templates.Fields = { 'LinkFilename': { 'View': PdfClientLinkFilenameNoMenu } };

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(PdfCtx);

})();



function GetExtension(szHref) {

    var sz = new String(szHref);

    var re = /^.*\.([^\.]*)$/;

    return (sz.replace(re, "$1")).toLowerCase();

}



var stsOpen = null;



function IsPdfClientInstalled() {

    if (stsOpen == null) {

        if (Boolean(window.ActiveXObject)) {

            try {

                stsOpen = new ActiveXObject("PdfFile.OpenDocuments");



            }

            catch (e) {

                stsOpen = null;

            }

        }

    }



    return (stsOpen != null);   

}



function OpenPdfInClient(pdfFileUrl) {   

    var fRet = true;



    try {

        fRet = typeof stsOpen.ViewDocument2 != "undefined" && stsOpen.ViewDocument2(window,

pdfFileUrl, '');

    }

    catch (e) {

        fRet = false;

    };



    if (event != null) {

        event.cancelBubble = true;

        event.returnValue = false;

    }



    return fRet;

}



function PdfNewGif(listItem, listSchema, ret) {

    if (listItem["Created_x0020_Date.ifnew"] == "1") {

        var spCommonSrc = GetThemedImageUrl("spcommon.png");



        ret.push("<span class=\"ms-newdocument-iconouter\"><img class=\"ms-newdocument-icon\"

src=\"");

        ret.push(spCommonSrc);

        ret.push("\" alt=\"");

        ret.push(Strings.STS.L_SPClientNew);

        ret.push("\" title=\"");

        ret.push(Strings.STS.L_SPClientNew);

        ret.push("\" /></span>");

    }

}



function PdfClientLinkFilenameNoMenu(param1, param2, listItem, listSchema) {

    var ret = [];

    var fileUrl = listItem.FileRef;



    if (fileUrl != null && typeof fileUrl != 'undefined' && TrimSpaces(fileUrl) != "") {

        if (listItem.FSObjType == '1') {

            if (listSchema.IsDocLib == '1') {

                RenderDocFolderLink(ret, listItem.FileLeafRef, listItem, listSchema);

            }

            else {

                RenderListFolderLink(ret, listItem.FileLeafRef, listItem, listSchema);

            }

        }

        else {

            ret.push("<a class='ms-listlink' href=\"");

            ret.push(listItem.FileRef);

            ret.push("\" onmousedown=\"return VerifyHref(this,event,'");

            ret.push(listSchema.DefaultItemOpen);

            ret.push("','");

            ret.push(listItem["HTML_x0020_File_x0020_Type.File_x0020_Type.mapcon"]);

            ret.push("','");

            ret.push(listItem["serverurl.progid"]);

            ret.push("')\" onclick=\"");



            var appInstalled = IsPdfClientInstalled();

            var szExt = GetExtension(listItem.FileRef);

            if (appInstalled && szExt == 'pdf' && browseris.ie) {

                ret.push("return OpenPdfInClient('");

                ret.push("http://");

                ret.push(window.location.hostname);

                ret.push(listItem.FileRef);

            }

            else {

                ret.push("return DispEx(this,event,'TRUE','FALSE','");

                ret.push(listItem["File_x0020_Type.url"]);

                ret.push("','");

                ret.push(listItem["File_x0020_Type.progid"]);

                ret.push("','");

                ret.push(listSchema.DefaultItemOpen);

                ret.push("','");

                ret.push(listItem["HTML_x0020_File_x0020_Type.File_x0020_Type.mapcon"]);

                ret.push("','");

                ret.push(listItem["HTML_x0020_File_x0020_Type"]);

                ret.push("','");

                ret.push(listItem["serverurl.progid"]);

                ret.push("','");

                ret.push(Boolean(listItem["CheckoutUser"]) ? listItem["CheckoutUser"][0].id :

'');

                ret.push("','");

                ret.push(listSchema.Userid);

                ret.push("','");

                ret.push(listSchema.ForceCheckout);

                ret.push("','");

                ret.push(listItem.IsCheckedoutToLocal);

                ret.push("','");

                ret.push(listItem.PermMask);

            }



            ret.push("')\">");



            var fileRef = listItem["FileLeafRef"];



            if (fileRef != null) {

                var index = fileRef.lastIndexOf('.');

                fileRef = index >= 0 ? fileRef.substring(0, index) : fileRef;

            }



            ret.push(fileRef);

            ret.push("</a>");



            PdfNewGif(listItem, listSchema, ret);

        }

    }

    else {

        ret.push("<nobr>");

        ret.push(listItem["FileLeafRef"]);

        ret.push("</nobr>");

    }



    return ret.join('');

}

****

Now, we Powershell.

Run the following Powershell commands, on a per-Library basis, to force PDFs to use this Javascript to open the files in the client. 

add-pssnapin microsoft.sharepoint.powershell

$web = Get-SPWeb http://somesharepointsite.com/sites/teamsite
$list = $web.Lists["My PDF Documents"]
$field = $list.Fields.GetFieldByInternalName("LinkFilename")
$field.JSLink = "/_layouts/15/PdfFix.js"
$field.Update($true)

***

Once that script runs, it'll be active.  No need to IIS reset or recycle application pools.

ONE IMPORTANT NOTE:  You're dumping customizations directly to the /15 hive.  This means Cumulative Updates / Service Packs will ALMOST CERTAINLY break this function.

We had the unfortunate experience of enabling this, then immediately patching to the May Cumulative Update the following weekend.  We had to re-run the Powershell script to re-enable the function (the .js file was not deleted).