Mittwoch, 11. Oktober 2017

In VisualStudio SharePoint-Projekt die .wsp automatisch erstellen bei (re)Build

Ich mag es wenn meine wsp's aktuell sind. Ich finde es doof, nach einem (re)Build immer noch einmal "Publish" oder (mit CKSDev) "Package all SharePoint-Projects" manuell starten zu müssen. Zumal ich das dann ständig vergesse.... Daher versuche ich immer die .wsp's direkt nach dem Build erzeugen zu lassen.

Einfach in der csproj-Datei die folgende Zeile finden (i.d.R am Ende der Datei):
<Import Project="$(VSToolsPath)\SharePointTools\Microsoft.VisualStudio.SharePoint.targets" Condition="'$(VSToolsPath)' != ''" />

und anschließend die folgenden Zeilen anfügen (und damit die "dependencies" für den Build-Schritt ändern):

<PropertyGroup>
 <BuildDependsOn>
  $(BuildDependsOn);
  CreatePackage;
 </BuildDependsOn>
</PropertyGroup>

formatierten Text an ein Word-Dokument anfügen

Die Aufgabe des Tages heute: Text mit Formatierungen (vorzugsweise HTML) an ein Word-Dokument anfügen.

Die Lösung ist eigentlich recht einfach:
Das HTML wird als AlternativeFormatImportPart dem Dokument hinzugefügt und dieser dann im Körper des Dokumentes am Ende hinzugefügt.
Bei Verwendung des OpenXmlSdk sieht das wie folgt aus:

// open some docx-document
using (var docx = WordprocessingDocument.Open(newDoc, true))
using (var htmlStream = new MemoryStream(htmlBytes))
{
  var mainDoc = docx.MainDocumentPart;
  // some id to use for adding first & referencing later
  var partId = string.Format("SPAdded{0:N}",Guid.NewGuid());

  // the new AlternativeFormatImportPart
  var part = mainDoc.AddAlternativeFormatImportPart(AlternativeFormatImportPartType.Html, partId);
  part.FeedData(htmlStream);

  // an AltChunk (referencing the AlternativeFormatImportPart by use of ID) 
  var partRef = new AltChunk {Id = partId};

  // add the AltChunk after the last element of the document
  var body = mainDoc.Document.Body;
  body.InsertAfter(partRef, body.Elements().Last());

  // save the document
  mainDoc.Document.Save();
}
In heutigen Fall musste das alles natürlich aus dem SharePoint erfolgen und das zu verändernde Dokument als neue Version im SharePoint abgelegt werden.
Meine Lösung ist ein WebService, der entsprechend (client-Seite, Workflow, etc...) verwendet werden kann.
"Fertig" sieht das dann wie folgt aus:

public void AppendContent(string urlToDocInSharePoint, string contentToAdd)
{
 if (string.IsNullOrEmpty(urlToDocInSharePoint))
 {
  throw new ArgumentException("file-url must be given.", "urlToDocInSharePoint");
 }
 if (string.IsNullOrEmpty(contentToAdd))
 {
  // nothing to do..
  return;
 }

 using (var site = new SPSite(urlToDocInSharePoint))
 using (var web = site.OpenWeb())
 {
  var webAppUrl = site.Url.Replace(web.ServerRelativeUrl, string.Empty);
  var siteRelativeDocumentUrl = urlToDocInSharePoint.Replace(webAppUrl, string.Empty);
  if (!siteRelativeDocumentUrl.StartsWith("/"))
  {
   siteRelativeDocumentUrl = "/" + siteRelativeDocumentUrl;
  }

  var file = web.GetFile(siteRelativeDocumentUrl);
  if (!file.Exists)
  {
   throw new FileNotFoundException(string.Format("The given file \"{0}\" in site \"{1}\" with relative url \"{2}\" could not be found. No such file exists.", 
    urlToDocInSharePoint, site.Url, siteRelativeDocumentUrl));
  }

  if (!file.InDocumentLibrary)
  {
   throw new ArgumentException(string.Format("The given file \"{0}\" is not in a DocumentLibrary. Unable to modify.", siteRelativeDocumentUrl), "context");
  }

  var html = contentToAdd;
  if (!html.StartsWith("<html", StringComparison.InvariantCultureIgnoreCase))
  {
   html = string.Format("<html>{0}</html>", html);
  }

  // according to https://stackoverflow.com/questions/18089921/add-html-string-to-openxml-docx-document the html-bytes must be UTF8-encoded with Preamble...
  var htmlBytes = new UTF8Encoding(true).GetPreamble().Concat(Encoding.UTF8.GetBytes(html)).ToArray();

  using (var newDoc = new MemoryStream())
  {
   // copy file from SP to memory
   using (var spStream = file.OpenBinaryStream(SPOpenBinaryOptions.None))
   {
    spStream.CopyTo(newDoc);
   }

   newDoc.Seek(0, 0);

   //open & modify docx
   using (var docx = WordprocessingDocument.Open(newDoc, true))
   using (var htmlStream = new MemoryStream(htmlBytes))
   {
    var mainDoc = docx.MainDocumentPart;

    // add html as "alternative format" to document, then reference it in the main body....
    var partId = string.Format("SPAdded{0:N}",Guid.NewGuid());

    var part = mainDoc.AddAlternativeFormatImportPart(AlternativeFormatImportPartType.Html, partId);
    part.FeedData(htmlStream);

    var partRef = new AltChunk {Id = partId};

    var body = mainDoc.Document.Body;
    body.InsertAfter(partRef, body.Elements().Last());

    mainDoc.Document.Save();
   }
   newDoc.Seek(0, 0);

   //push newDoc as new Version to list
   var folder = file.ParentFolder;
   if (folder.RequiresCheckout)
   {
    file.CheckOut();
   }

   var uploaded = folder.Files.Add(file.Url, newDoc, true);
   uploaded.Update();

   if (folder.RequiresCheckout)
   {
    uploaded.CheckIn("Updated via WebService", SPCheckinType.MajorCheckIn);
    uploaded.Publish("Updated via WebService");
   }
  }
 }
}
Das ganze hätte noch etwas generischer gefasst werden können - war aber so vorerst ausreichend.

Mittwoch, 9. August 2017

Alle laufenden Crawls stoppen

Schnell mal alle laufenden Crawls in der SharePoint-Farm stoppen?
Einfach:

Inhalt des SharePoint SecureStore auflisten

Den Inhalt des SecureStore auflisten ist im Grunde keine gute Idee. Immerhin ist der Inhalt "sicher"..
Manchmal muss aber trotzdem einen Blick darauf werfen - entweder weil keiner die Einstellungen der DEV-VM dokumentiert hat oder weil der Kunde sich gerade nicht sicher ist was denn nun eingestellt ist...

Den Inhalt aufzulisten ist per PowerShell einfach möglich:

Freitag, 21. Oktober 2016

Auflistung aller e-Mail fähigen Listen in der SharePoint-Farm

Es sollen "mal schnell" alle Listen & Bibliotheken aufgelistet werden, für die ein e-Mail Empfang konfiguriert ist? Nichts leichter als das:


<#
  .SYNOPSIS
   Lists all E-Mail enabled lists
   If all is fine here you'll need to check
    Resolve-DnsName <your-mail-domain-here> -Type MX #make sure the "right" server is the MX
 Get-WindowsFeature smtp-server #make sure InstallState is "installed"
 Get-Service SMTPSVC #make sure Status is "running"

  .EXAMPLE
   Get-SPEmailEnabledLists
   Gets all e-Mail enabled lists from all webs in all site collections from all WebApplications. This may be a lot to check: You have been warned.

  .EXAMPLE
   Get-SPEmailEnabledLists -WebApplication (Get-SPWebApplication http://sp.dev/)
    Gets all e-Mail enabled lists from all webs in all site collections of the given WebApplication

  .EXAMPLE
   Get-SPSite | ?{$_.Url - match "my."} | Get-SPEmailEnabledLists
    Gets all e-Mail enabled lists from all webs in the given site collections

  .EXAMPLE
   Get-SPWeb http://sp.dev/sites/simple/sub | Get-SPEmailEnabledLists
    Gets all e-Mail enabled lists from the given web
#>
[CmdletBinding()]
param(
    [Parameter(
        HelpMessage="restrict search to this web",
        ValueFromPipeline=$true)]
    [Microsoft.SharePoint.SPWeb[]]$Web,
    [Parameter(
        HelpMessage="restrict search to this site collection",
        ValueFromPipeline=$true)]
    [Microsoft.SharePoint.SPSite[]]$Site,
    [Parameter(
        HelpMessage="restrict search to this webApplication",
        ValueFromPipeline=$true)]
    [Microsoft.SharePoint.Administration.SPWebApplication[]]$WebApplication
)

Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction Inquire | Out-Null

# check incoming mail settings
$serverAddr = "<unconfigured>";
$settings = (Get-SPFarm).Services | ?{ $_.TypeName -match "incoming e-mail" } | select -First 1
if((-not $settings) -or (-not $settings.Enabled)){
    $url = "_admin/IncomingEmail.aspx";
    $ca = (Get-SPWebApplication -IncludeCentralAdministration) | ?{ $_.IsAdministrationWebApplication } | select -First 1
    if($ca) {
        $url = $ca.Url + $url;
    } else {
        $url = "http://<your central admin>/$url";
    }
    Write-Warning "No incoming email-settings found, or incoming email is deactivated. check $url !"
} else {
    $serverAddr = $settings.ServerDisplayAddress;
}

# collect the lists to check
if(-not $Web) {
    Write-Verbose "no web given - getting all webs from site";
    if(-not $Site) {
        Write-Verbose "no site given - getting all sites from webApplication";
        if(-not $WebApplication) {
            Write-Verbose "no webApplication given - getting all webApplications";
            $WebApplication = Get-SPWebApplication;
        }

        $Site = $WebApplication | % { $_.Sites };
    }

    $Web = $Site | %{ $_.AllWebs };
}


# check the lists
$lists = $Web | %{ $_.Lists }
Write-Verbose "checking $($lists.Count) lists...";
$lists | %{
    if(($_.CanReceiveEmail) -and ($_.EmailAlias)) {
        [PSCustomObject]@{
            Title = $_.Title;
            Url = "$($_.ParentWeb.Url)/$($_.RootFolder.Url)";
            EmailAlias = "$($_.EmailAlias)@$serverAddr";
        }
    }
}


Das Skript kann als Eingabe für Out-GridView oder auch Export-Csv verwendet werden - oder auch nur in der Console betrachtet...
Und so ganz nebenbei prüft das Skript noch ob der e-Mail Empfang in der Farm überhaupt eingerichtet ist..

Dienstag, 17. Mai 2016

SharePoint eine Test-Mail versenden lassen

Auf die spannende Frage "Warum versendet SharePoint keine Mails?" gibt es in der Regel nur wenige Antworten.
Wenn die Einstellungen der ausgehenden Mails ("Configure outgoing e-mail settings") in SharePoint gut aussehen hilft meistens nur testen und das ULS-Log beobachten.

Das folgende PowerShell-Skript kann verwendet werden um schnell Test-Mails aus dem SharePoint versenden zu lassen (Dabei kann man dann das ULS-Log - am besten mittels ULS Viewer - beobachten)
function Send-SPTestMail {
  <#
      .SYNOPSIS
        Sends a Test-Mail using SharePoint-Standard Tools
        returns "TRUE", if the mail was successfully "taken in" with SharePoint (most probably also given to smtp...)
        and "FALSE" if there was any failure to do so.
        Check your ULS-Logs for further investigation if the result is FALSE, or your smtp-server-logs if the result is TRUE
  
      .PARAMETER To
        "TO" field of the mail.

      .PARAMETER Web
        The Url to the SPWeb to access via the SharePoint e-Mail utilities

      .PARAMETER From
        "FROM" field of the mail. defaults to "someone@example.com"
      
      .PARAMETER Subject
        "SUBJECT" field of the mail. Defaults to "Test" and a Date.

      .PARAMETER Body
        "BODY" of the mail. Defaults to some nice text.
  
      .EXAMPLE
        Send-SPTestMail -Web http://path.to/Web -From me@acme.com
        Sends a simple mail using all the nice deafults..
   #>
   [CmdletBinding()]
   param (
      [Parameter(Mandatory=$true)]
      [string]$Web,
      [Parameter(Mandatory=$true)]
      [string]$To,
      [Parameter(Mandatory=$false)]
      [string]$From = "someone@example.com",
      [Parameter(Mandatory=$false)]
      [string]$Subject = $null,
      [Parameter(Mandatory=$false)]
      [string]$Body = $null
   )

    Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction Stop
    $headers = new-object System.collections.Specialized.StringDictionary
    if(!$Subject) {
        $Subject = "Test @ $(Get-Date)";
    }

    $headers.add("to",$To)
    $headers.add("from",$From)
    $headers.add("Subject",$Subject)

    if(!$Body){
        $Body = "Auto-Generated body of test-mail <ul><li>Generated at $(Get-Date)</li><li>Headers:<ul>";
        $headers | % { $Body += "<li>$($_.Name): $($_.Value)</li>";  }
        $Body += "</ul></li><li>Sent via SharePoint-Web at $($Web)</li></ul>";
    }

    $spweb = $null;
    try {
        $spweb = Get-SPWeb $Web -ErrorAction Stop
        [Microsoft.SharePoint.Utilities.SPUtility]::SendEmail($spweb,$headers,$Body)
    } finally {
        if($spweb -ne $null) {
            $spweb.Dispose();
        }
    }
 }

Donnerstag, 28. April 2016

web.config-transform - "my way"

Ich mag die Web.Config-Transforms. Sehr sogar.
Was mich daran stört ist, dass ich eine Build-Konfiguration für jede Stage/jeden Server benötige. Um das zu umgehen (und ggf. vielleicht auch einmal andere xml-Dateien zu transformieren) kann die Transformation auch manuell über einen MSBuild-Task starten.
Im Regelfall habe ich meine Transformationen im "Configs\"-Verzeichnis mit der Benennung web.[stage].conf - die lasse ich dann automatisiert beim build & publish erstellen. Dafür habe ich das folgende targets-file, das ich einfach im csproj einbinde:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <SourceWebConfig>$(ProjectDir)\Web.config</SourceWebConfig>
        <OutputPath Condition="'$(OutputPath)' == ''">$(ProjectDir)\bin</OutputPath>
        <TempWebConfig>$(OutputPath)\Web.Temp.config</TempWebConfig>
        <TransformationsBaseDir>$(ProjectDir)\Configs</TransformationsBaseDir>
        <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">11.0</VisualStudioVersion>
        <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
    </PropertyGroup>
    <Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets"/>

    <ItemGroup>
        <WebConfigTransforms Include="$(TransformationsBaseDir)/*.config">
            <TransformDest>$(OutputPath)\%(WebConfigTransforms.Filename).config</TransformDest>
        </WebConfigTransforms>
    </ItemGroup>

    <Target Name="TransformAllWebConfigs">
        <!-- Does the real transformation of all web-config transforms... -->
        <Copy SourceFiles="$(SourceWebConfig)"
              DestinationFiles="$(TempWebConfig)" />
        <Message Text="transforming %(WebConfigTransforms.Identity) to %(WebConfigTransforms.TransformDest)"
                 Importance="high"/>
        <TransformXml Source="$(TempWebConfig)"
                      Transform="%(WebConfigTransforms.Identity)"
                      Destination="%(WebConfigTransforms.TransformDest)"
                      StackTrace="true" />
        <Delete Files="$(TempWebConfig)" />
    </Target>

    <Target Name="TransformAfterBuild"
            AfterTargets="Build">
        <!-- after build, build all web.configs, too -->
        <CallTarget Targets="TransformAllWebConfigs" />
        <Message Text="TransformAllWebConfigs has run after build!"
                 Importance="high"/>
    </Target>
    <Target Name="MimicVsTransforms"
            AfterTargets="PreTransformWebConfig">
        <!-- this should mimic the original VS-behaviour -->
        <CallTarget Targets="TransformAllWebConfigs" />
    </Target>
    <Target Name="CopyTransformedForPublish"
            AfterTargets="CopyAllFilesToSingleFolderForPackage">
        <!-- "GatherAllFilesToPublish" as AfterTarges seems to work, too. But only when Publish is called from within VS. -->
        <Copy SourceFiles="@(WebConfigTransforms->'%(TransformDest)')"
              DestinationFolder="$(WPPAllFilesInSingleFolder)" />
    </Target>
</Project>

Die Einbindung im projekt-file erfolgt ganz "normal".
    <Import Project="$(ProjectDir)\..\TransformWebConfig.targets" Condition="'$(TransformationsBaseDir)' == ''" />

In einem aktuellen Projekt bestand die Anforderung die Transformationen in Unterverzeichnissen abzulegen (da es noch mehr spezifische Dateien pro Stage gab...). Dafür habe einfach die ItemGruop wie folgt angepasst:
<ItemGroup>
 <WebConfigTransforms Include="$(TransformationsBaseDir)\**\web.config">
  <StageName>$([System.String]::new('%(RecursiveDir)').TrimEnd('\\'))</StageName>
  <TransformDest>$(OutputPath)\web.%(StageName).config</TransformDest>
 </WebConfigTransforms>
</ItemGroup>