Kategorien
IT SharePoint

SharePoint – Tenant-OU wieder freigeben

SharePoint 2016 hat (anders als SharePoint 2019) noch Multi-Tenancy-Unterstützung. Das heisst, man kann einige Service-Applikationen, wie z.B. den Managed Metadata Service oder den User Profile Service für verschiedene Tenants Partitionieren – im Prinzip das, was Microsoft auch in SharePoint Online macht, um verschiedenen Kunden einen eigenen Bereich in den Service-Applikationen zu geben.

Beim erstellen eines Tenants, wird mit New-SPSiteSubscription so eine Partition für den Tenant registriert.

Mann kann dann mit dieser Subscription z.B. den People Picker auf eine OU (Organizational Unit im Active Directory) einschränken:

Set-SPSiteSubscriptionConfig -identity $siteSubscription -FeaturePack $defaultfeaturePackId -UserAccountDirectoryPath "OU=UserAccounts,DC=FABRIKAM,DC=COM"

Beim Erstellen einer Site Collection für den Tenant wird diese Subscription auch mitgegeben:

New-SPSite -Url https://fabrikam.sptest.local" -SiteSubscription $siteSubscription -HostHeaderWebApplication "https://root.sptest.local"

Und mit folgendem Befehl Add-SPSiteSubscriptionProfileConfig wird eingestellt, dass für diese Subscription die AD-Benutzer mit der entsprechenden OU synchronisiert werden (für die User Profile Service Application).

$upaProxy = Get-SPServiceApplicationProxy | where-object { $_.DisplayName -eq "MT User Profile Service Application" }
Add-SPSiteSubscriptionProfileConfig -Identity $siteSubscription -SynchronizationOU "OU=UserAccounts,DC=FABRIKAM,DC=COM" -ProfileServiceApplicationProxy $upaProxy

Wenn man zu einem späteren Zeitpunkt den Tenant löschen möchte, zum Beispiel weil beim Erstellungsprozess etwas schief gelaufen ist, dann muss man schlau vorgehen, damit die OU wiederverwendet werden kann. Eine OU kann nämlich nicht von mehr als einer Subscription verwendet werden.

Mit folgenden Schritten (in der SharePoint Management Shell) kann man eine Subscription und die zugehörige Subscription-Profile-Config löschen. Achtung: es werden dabei alle Site Collections gelöscht, welche zur jeweiligen Subscription gehören!

Zuerst holt man sich die jeweilige Subscription, welche gelöscht werden soll, zum Beispiel über den URL der Site:

# Site holen
$site = Get-SPSite "http://fabrikam.sptest.local"

# subscription der Site holen
$subscription = $site.SiteSubscription

Dann muss man sich die User Profile Service Application holen (darauf achten, dass die richtige Multi-Tenancy-fähige Service-Applikation geholt wird, falls ihr mehere, z.B. auch eine normale, in der Farm habt).

# Namen des UPA Proxys angeben
$upaProxyName = "MT User Profile Service Application"

# UPA Proxy holen
$upaProxy = Get-SPServiceApplicationProxy | where-object { $_.DisplayName -eq $upaProxyName }

Nun kann man als erstes die OU-Synchronisierung entfernen, es darf auf keinen Fall zuerst die Subscription direkt gelöscht werden, denn das wäre möglich und würde das ausführen dieses Befehls verunmöglichen, was dazu führt, dass man die OU nur noch über die manuelle Bearbeitung der Datenbank erledigen kann (was Microsoft nicht gerne hat):

# UPA Subscription Eintrag entfernen (aus UPA Profile DB für Profile Sync.)
Remove-SPSiteSubscriptionProfileConfig -Identity $subscription -ProfileServiceApplicationProxy $upaProxy

Nun kann die Subscription gelöscht werden. Um Sicherzugehen, welche Sites betroffen sind, kann man zuerst $subscription.Sites aufrufen:

# Site subscription und alle assozierten Sites löschen
Remove-SPSiteSubscription -Identity $subscription

Am Ende kann noch die Inhaltsdatenbank gelöscht werden, in welcher die Site Collection für den Tenant gespeichert war (sinnvoll falls man pro Tenant eine eigene Inhaltsdatenbank verwendet):

# Zum Schluss die Inhaltsdatenbank entfernen. Auch hier sollte man zuerst prüfen, dass keine anderen Sites in der Inhaltsdatenbank sind! (Befehl geht auch, wennd die Site zuvor gelöscht wurde, das Objekt hat noch den Wert der Datenbank und den Datenbanknamen)
$databaseName = $site.ContentDatabase.Name
Remove-SPContentDatabase -Identity $databaseName

Dies war der Weg um einen Tenant sauber zu löschen, so dass die OU die verwendet wurde, auch später wieder neu verwendet werden kann.

Falls man diesen Weg nicht eingehalten hat und z.B. die Site-Subscription schon gelöscht hat, ohne zuerst Remove-SPSiteSubscriptionProfileConfig auszuführen, dann bleibt einem nur noch der Weg, die Subscription manuell aus der Datenbank zu entfernen, dies führt gemäss Microsoft zum Verlust der Garantie!

Es handelt sich dabei um die Profile DB der User Profile Service Application, in welcher in diversen Tabellen die Einträge für unsere Subscription (Partition-ID) gelöscht werden müssen.

Nachfolgend ein SQL-Script, mit welchem man dies machen könnte, Verwendung aber auf eigene Gefahr. Wichtig ist, dass ihr in der ersten Zeile den Wert der Variable SubscriptionId auf die entsprechende Subscription-ID setzt und danach natürlich [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa] mit der ensprechenden Datenbank und dem entsprechenden Schema für euren Fall:

DECLARE @SubscriptionId AS Varchar(255) = '6d947c20-f876-4d4b-b680-1f0cd0a78c6f'

Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ProfileSubtypeList WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].PersonalSite WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Orgle_Stats WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Orgle_OpList WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Orgle_List WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Orgle_ErrorMsg WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].InternalDetail WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].DataTypeList WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].DataServiceList WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].BucketInfo WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].MicrofeedPostDefinitions WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ActivityTemplate WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ActivityApplication WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ActivityType WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ActivityApplication WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Profile_SearchStopwords WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Profile_SearchUserTable WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].OrganizationProfileValue WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].OrganizationProfile WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].UserProfileEventLog WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].UserProfileValue WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].UserProfile_Full WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].UserPrivacyPolicy WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ADImportConstantPropertyMapping WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ADImportPropertyMapping WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ProfileSubtypePropertyAttributes WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ProfileTypePropertyAttributes WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].PropertyList WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].[Profile_Stats] WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Tenants WHERE PartitionID = @SubscriptionId

Es empfiehlt sich danach etwas zu warten, da diese Änderungen nicht sofort ziehen. Im Idealfall könnt ihr danach einen IISReset auf allen SharePoint-Servern durchführen und ich würde auch die Instanz auf dem Datenbankserver neu starten.

Kategorien
Software UWP

Qooli GIF Editor / Qooli Time Tracker

Für meine beiden Universal Windows Platform Apps (UWP) Qooli Time Tracker und Qooli GIF Editor habe ich je eine dedizierte Seite (auf Englisch) erstellt, mit Informationen zu den jeweiligen Anwendungen:

  • Kurze Beschreibung, für was die App verwendet werden kann
  • Screenshots
  • Liste mit den wichtigsten Features
  • Link zur Seite der App im Microsoft Store
  • Hinweistext bezüglich Support/Unterstützung/Bugs und Feature Requests

Als Goodie findet ihr zudem unten in diesem Blog-Post einige Promo-Codes, um die beiden Anwendungen kostenlos herunterzuladen.

Auch die Seite Software wurde aktualisiert, dort findet ihr eine Kurzbeschreibung (auf Deutsch) für einige ausgewählte Apps aus meinem Portfolio. Einige alte Einträge habe ich entfernt, da sie nicht mehr zeitgemäss waren.

Die Seiten sind in Englisch geschrieben, da die Store-Einträge auf diese verlinken, vielleicht folgt in der Zukunft noch eine veutsche Version der jeweiligen Seite.

Nachfolgend noch ein paar Informationen auch hier, zu den beiden UWP Apps.

Qooli GIF Editor

Mit dieser Anwendung können GIF-Dateien geöffnet und bearbeitet werden. Das heisst, man kann die Frames löschen, verschieben, neue Frames hinzufügen.

Qooli GIF Editor
Qooli GIF Editor

Man kann natürlich auch bei null anfangen und ein neues GIF erstellen indem man entweder aus Einzelbildern ein GIF zusammenstellt oder zum Beispiel eine Szene aus einer Video-Datei extrahiert.

Qooli GIF Editor - Video-Frame-Extraktion
Qooli GIF Editor – Video-Frame-Extraktion

Es ist auch möglich die Einzelbilder einer Datei zu exportieren, entweder alle oder nur die gewünschten.

Ein besonderes Feature ist „Auto-Smooth“, mit einem Knopfdruck werden dem GIF am Ende die Bilder in umgekehrter Reihenfolge so hinzugefügt, dass ein nahtloser Übergang zum Start erreichert wird.

Qooli GIF Editor - "Auto Smooth"-Beispiel
Qooli GIF Editor – „Auto Smooth“-Beispiel

Mit einem Wasserzeichen-Editor, kann man einen benutzerdefinierten Text im GIF-Bild platzieren, die Farbe, Schriftart und Schriftgrösse anpassen und auch entscheiden ob eine Kontur für die Schrift verwendet werden soll.
Der Text kann im Bild frei positioniert werden.

Qooli GIF Editor - Wasserzeichen-Editor
Qooli GIF Editor – Wasserzeichen-Editor

Das resultierende Bild kann dann natürlich aus der Anwendung als GIF-Datei abgespeichert werden.

Mehr Informationen über den Qooli GIF Editor findet ihr hier: https://qoo.li/software/qooli-gif-editor/

Die Anwendung kostet aktuell im Store ungefähr 3.90 CHF – mit einem der nachfolgenden Promo-Codes, könnt ihr sie jedoch kostenlos herunterladen (ich werde die gebrauchten Codes nach und nach durchstreichen).

Promo Codes für Qooli GIF Editor

Qooli Time Tracker

Mit dem Qooli Time Tracker kann man tracken, für welche Aufgaben oder Projekte man seine Arbeitszeit aufwendet.

Qooli Time Tracker
Qooli Time Tracker

Man kann einfach erfassen, wann man eine Aufgabe startet und erhält dann automatisch eine Übersicht über die Summen pro Aufgabe/Projekt und die Pausen.

Auch sieht man wieviele Stunden insgesammt schon am Tag geleistet wurden und wann das Tagesziel erreicht ist.

Mit Navigations-Funktionen, kann man bequem und einfach zwischen verschiedenen Daten hin und her navigieren.

Es gibt auch eine Wochenansicht, welche einem die Summen über die einzelnen Aufgaben/Projekte pro Woche und Wochentag aufzeigt, auch hier kann man einfach zur gewünschten Woche wechseln.

Qooli Time Tracker – Wochenübersicht

Im Weiteren gibt es noch eine Export- und Import-Funktion, so können die erfassten Daten als JSON-Datei exportiert werden und auch wieder importiert werden. Man könnte so die Daten auch an ein Drittsystem übergeben.

Qooli Time Tracker - JSON Export/Import
Qooli Time Tracker – JSON Export/Import

Mehr Informationen über den Qooli Time Tracker findet ihr hier:
https://qoo.li/software/qooli-time-tracker/

Die Anwendung kostet aktuell im Store ungefähr 1.90 CHF – mit einem der nachfolgenden Promo-Codes, könnt ihr sie jedoch kostenlos herunterladen (ich werde die gebrauchten Codes nach und nach durchstreichen).

Promo Codes fü Qooli Time Tracker

Kategorien
IT SharePoint

SharePoint (On-Premise) Blob-Cache löschen

Da sich der Markt in den letzten Jahren sehr in Richtung Office 365 bewegt hat, habe ich nur noch selten mit On-Premise SharePoint-Installationen zu tun.

Heute jedoch, musste ich eine (SharePoint 2016) Test-Farm neu aufbauen, zumindest ab Stufe Web-Applikation/Inhaltsdatenbank, welche neu von der Produktion übernommen werden musste und dann als neues Test-System konfiguriert werden sollte, da die alte Test-Farm nach einem missglückten Update korrupt und nicht mehr lauffähig war.

Um zu erkennen, ob man sich auf der produktiven Seite oder auf der Test-Umgebung befindet, hatten wir unteranderem früher via Composed Looks in der Test-Umgebung einen eigenen Look definiert, der im Farbton von der produktiven Seite abwich.

Im Prinzip also ein Look, mit der selben Masterpage, aber einem anderen *.spcolor-File, welches für das Test-System andere Farbtöne enthielt.

Nach dem Einspielen der neuen *.spcolor-Datei und aktivieren des gewünschten Composed Looks stellte sich jedoch Ernüchterung ein, die Seite sah genau so aus wie vorher. In der Preview des Composed Looks war schon zu erkennen, dass sich nichts ändern würde.

Gut, an was könnte das liegen? SPColor-Datei eingecheckt und veröffentlicht? Ja. Composed Look nochmals wegwechseln und dann erneut probieren – brachte nichts. IISReset durchführen – brachte auch nichts? Vielleicht den Object-Cache auf allen Servern (hier gab es nur einen) leeren? Hat auch nichts gebracht.

Eine Überprüfung der SPColor-Datei zeigte, dass sie die richtigen Werte enthielt, sie war auch veröffentlicht. Mit den Developer Tools im Browser wurde dann geprüft, durch welche CSS-Regel dieser Style appliziert wurde.

Der (falsche) Style entstammte von einer Themed-CSS-Datei. Beim Anwenden eines Composed Looks, werden die Werte aus der SPColor-Datei in eine zum Theme zugehörige CSS-Datei geschrieben (/Catalog-Bibliothek), die Datei hatte auch den richtigen Namen, aber die falschen Werte die nicht mit denen aus der SPColor-Datei übereinstimmten.

Ich erinnerte ich mich daran, dass ich so ein Problem schon einmal hatte und es daran lag, dass im Blob-Cache noch alte Versionen der CSS-Dateien (und anderer Dateien) vorhanden waren und diese dann an den Browser gesendet wurden, anstelle der aktuellen/korrekten.

Zuerst habe ich versucht, den Blob-Cache mittels PowerShell (SharePoint Management Shell) zu leeren:

$webApp = Get-SPWebApplication "<WebApplicationURL>"
 [Microsoft.SharePoint.Publishing.PublishingCache]::FlushBlobCache($webApp)

Leider hatte auch das nichts gebracht, nach einem erneuten aktivieren des gewünschten Looks, war immer noch alles beim Alten. Auch nach einem erneuten IISReset.

Ich wusste, dass man den Cache auch manuell löschen kann. Um auf Nummer sicher zu gehen, dass es wirklich nicht an diesem Cache liegt, beschloss ich auch dies auszuprobieren.

Dazu mus man wie folgt vorgehen:

  • web.config Datei der Web-Applikation ermitteln, öffnen und darin den Blob-Cache deaktivieren (Tag „BlobCache“ suchen und das Attribut „enabled“ auf „false“ stellen – dies muss auf jedem SharePoint-Server der Farm durchgeführt werden
  • Den Ordner des Blob-Caches im Dateisystem auf jedem Server löschen (den Pfad zum Ordner findet ihr auch in der web.config, im BlobCache-Tag)
  • IISReset durchführen
  • Nun den Blob-Cache wieder auf jedem Server der Farm aktivieren, dazu die web.config-Dateien erneut anpassen (BlobCache-Tag Attribut „enabled“ nun auf „true“ setzen)
  • Erneut einen IISReset durchführen (sollte durch das Speichern der web.config eigentlich automatisch geschehen)

Nachdem ich diese Schritte durchgeführt hatte, ging ich in den Websiteeinstellungen erneut auf „Aussehen ändern“ (Composed Looks) und wählte den gewünschten Look aus. Im Preview erschien immer noch der falsche Farbton. Ich habe den Look trotzdem angewendet und siehe da, auf der Webseite war dann nun doch der richtige Farbton endlich vorhanden.

Die (themed) CSS-Datei wurde nun korrekt neu erstellt, mit dem gewünschten, angepassten Farbton.

Hier noch weitere Informationen zum Thema Blob Cache, auf dem Blog von Karin Bosch, dort musste ich auch wieder einige der manuellen Schritte nachschlagen, da ich sie selber nicht mehr alle wusste 🙂 :
https://karinebosch.wordpress.com/my-articles/improving-performance-of-sharepoint-sites/part-3-blob-caching-in-sharepoint-2010/

Kategorien
IT SharePoint

SharePoint: client-seitiger Datei-Upload mit Metadaten

Vor ein paar Monaten bekam ich eine Anfrage, ein Skript zu entwickeln, um eine Datei mit Metadaten in eine SharePoint-Dokumentbibliothek hochzuladen.

Das Skript würde durch eine Workflow-Anwendung angestossen werden, welche unter anderem PowerShell-Skripts starten und an diese Parameter übergeben kann.

Diese Anwendung lief jedoch nicht auf einem SharePoint-Server, so war es mir nicht möglich das Server Side Object Model zu verwenden, welches ich im Normalfall für ein solches Szenario gewählt hätte und welches ich auch heute noch bevorzuge, da es noch immer mächtiger ist (als das Client Side Object Model) und man auf das ganze Async-„Gemurks“ verzichten kann.

Der eigentliche Datei-Upload gestalltet sich mit CSOM auch äusserst einfach. Was mir aber grosse Probleme bereitete, war die Übergabe von Benutzer-Logins an das Skript, respektive, dass diese dann korrekt verwendet wurden.

In den ersten Anläufen hatte ich das Phänomen, dass wenn ich Benutzer-Logins (via String-Array) übermittelte, nur die ungeraden übernommen wurden.

Initial hatte ich das sehr dirty (Schande über mich) gelöst, indem ich einfach alle Einträge im Array jeweils verdoppelte.
Da ich aber solche Hacks nicht sehr sauber finde und diese ja nicht gerade sehr wartungsfreundlich sind, da wenn jemand der in einem Jahr den Code anschaut sich denkt „Wtf?! Warum?“, beschloss ich der Sache etwas tiefer auf den Grund zu gehen und eine bessere Lösung zu finden.

Leider schien ich mit dieser Problematik alleine zu sein. Ich fand im Internet zwar einige Beispiele für das setzen von Metadaten via CSOM bei Datei-Uploads, jedoch nie eine Beispiel für das setzen eines „People Picker“-Feldes, welches mehrere Werte erlaubte (FieldUserValueCollection).

Nach etwas Debugging und Ausprobieren konnte ich das Problem aber dann doch noch eingrenzen und schlusssendlich lösen.
Das Problem lag daran, dass bei den Einträgen für die Benutzer auch ein Wert (LookupValue) vorhanden sein muss (welcher, wenn ich mich richtig erinnere, nicht gesetzt werden konnte, da der Setter read-only ist).
Der Feldwert darf dann nicht einfach als FieldUserValue-Array gesetzt werden, sondern muss folgendes Format haben, welches ihr vielleicht schon kennt, wenn ihr auf einem Lookup-Wert schon einmal „.ToString()“ ausgeführt habt („[User-ID];#[User-Anzeigename]“):

"32;#Peter Muster;#22;#Petra Meier;#31;#Hans Meister;#"

Hier der Auszug der relevanten Stelle, mit der Erstellung eines „FieldUserValue“-Objektes (gut könnte man strenggenommen auch weglassen und den $valueUsers-String direkt aufbauen) und der anschliessenden Umwandlung der Collection in einen String:

{
    ...
    $user = $web.EnsureUser($userLogin)
    # User must be loaded to pass a valid user ID as lookup ID
    $ctx.Load($user)
    $ctx.ExecuteQuery()
    if ($user.Id -ne $null)
    {
        $fieldUserValue = New-Object Microsoft.SharePoint.Client.FieldUserValue
        $fieldUserValue.LookupId = $user.Id
        $userFieldValueCollection += $fieldUserValue
    }
    else
    {
        throw "User for login '$userLogin' not found!"
    }
}

$valueUsers = ($userFieldValueCollection | % { $_.LookupId.ToString() + ";#DUMMY;#" }) -join "" 

Hier noch das komplette Beispiel-Skript:

param(
    [Parameter(mandatory=$true)]
    [string] $SourceFilePath, 
    [Parameter(mandatory=$true)]
    [string] $ValueTitle,
    [Parameter(mandatory=$true)]
    [string] $ValueChoice,
    [Parameter(mandatory=$false)]
    [string] $ValueComments,
    [Parameter(mandatory=$false)]
    [bool] $ValueSendNotification = $true,
    [Parameter(mandatory=$true)]
    [string[]] $ValueUserLogins
)

# Set target site collection URL
$siteUrl = "http://server.name"
$targetLibraryInternalName = "Documents"

# Get script directory
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path

# Load SharePoint Client Dlls
Add-Type -Path (Resolve-Path (Join-Path -Path $ScriptDir -ChildPath "Microsoft.SharePoint.Client.dll"))
Add-Type -Path (Resolve-Path (Join-Path -Path $ScriptDir -ChildPath "Microsoft.SharePoint.Client.Runtime.dll"))

# Create Client Context (connect to target site)
$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($siteUrl)
$web = $ctx.Web 
$ctx.Load($web) 
$ctx.ExecuteQuery()
Write-Host "Connected to web: $($web.Url)" -ForegroundColor Yellow

# Get target library
$targetLibrary = $ctx.Web.GetList($web.ServerRelativeUrl + "/" + $targetLibraryInternalName)

# Prepare 'Viewers' value
[Microsoft.SharePoint.Client.FieldUserValue[]]$userFieldValueCollection = @()
foreach ($userLogin in $ValueUserLogins)
{
    $user = $web.EnsureUser($userLogin)
    # User must be loaded to pass a valid user ID as lookup ID
    $ctx.Load($user)
    $ctx.ExecuteQuery()
    if ($user.Id -ne $null)
    {
        $fieldUserValue = New-Object Microsoft.SharePoint.Client.FieldUserValue
        $fieldUserValue.LookupId = $user.Id
        $userFieldValueCollection += $fieldUserValue
    }
    else
    {
        throw "User for login '$userLogin' not found!"
    }
}
# Needs beside the LookupId also a dummy LookupValue, otherwise only odd entries are stored!
$valueUsers = ($userFieldValueCollection | % { $_.LookupId.ToString() + ";#DUMMY;#" }) -join "" 

# Upload file
$fci = New-Object Microsoft.SharePoint.Client.FileCreationInformation
$fci.Overwrite = $true
$fci.Content = [System.IO.File]::ReadAllBytes($SourceFilePath)
$fci.URL = [System.IO.Path]::GetFileName($SourceFilePath)
$uploadFile = $targetLibrary.RootFolder.Files.Add($fci)
Write-Host "File uploaded to target library"

# Set metadata properties on uploaded file (must be done on item of file)
Write-Host "Writing metadata to fields"
$listItem = $uploadFile.ListItemAllFields
$listItem["Title"] = $ValueTitle
$listItem["MyChoice"] = $ValueChoice
$listItem["Comments"] = $ValueComments
$listItem["SendNotification"] = $ValueSendNotification
$listItem["Readers"] = $valueUsers
$listItem.Update()
Write-Host "Executing pending queries (write metadata to file)"

$ctx.Load($uploadFile)
$ctx.Load($listItem)
$ctx.ExecuteQuery()

Write-Host "Uploaded file '$($uploadFile.Name)', received ID: $($listItem.Id)" -ForegroundColor Yellow

Das Skript kann wie folgt aufgerufen werden:

.\Skript.ps1 -SourceFilePath "X:\SourceFiles\Test Document.docx" -ValueTitle "Title Value" -ValueChoice "My Choice Value" -ValueComments "Test Comment" -ValueUserLogins @("Domain\username1", "Domain\username2") –ValueSendNotification $true 

Wichtig: damit ihr via PowerShell CSOM verwenden könnt, müsst ihr noch die SharePoint-Client-DLLs zur Verfügung haben (findet ihr im SharePoint-Installationsverzeichnis) und diese im Skript Verzeichnis ablegen, darum die beiden Aufrufe im Skript:

# Load SharePoint Client Dlls
Add-Type -Path (Resolve-Path (Join-Path -Path $ScriptDir -ChildPath "Microsoft.SharePoint.Client.dll"))
Add-Type -Path (Resolve-Path (Join-Path -Path $ScriptDir -ChildPath "Microsoft.SharePoint.Client.Runtime.dll"))

Kategorien
IT SharePoint

SharePoint: Degradierte Search-Index-Komponente korrigieren

Es kann manchmal vorkommen, dass eine Index-Partition der SharePoint Search Service Application degradiert, sich also in einem nicht mehr funktionstüchtigen Zustand befindet. Versucht man auf einer SharePoint-Seite dann eine Suche zu starten, wird man von einer Fehlermeldung begrüsst.

Der Administrator sieht dann in der Search Service Administration in der Central Administration ungefähr folgendes Bild:

Degraded SharePoint Index ComponentDas Ausrufezeichen signalisiert direkt, dass etwas nicht in Ordnung ist.

Um das Problem zu lösen, kann es genügen, die Index Component neu zu erstellen. Falls es daran liegt, dass die Disk, auf welcher die Index-Partition liegt, vollgelaufen ist, kann man auch eine zusätzliche Index-Komponente für die Partition auf einer anderen Disk erstellen.

Um eine neue Komponente zu erstellen, muss man PowerShell verwenden. Wie bei allen Änderungen an der Search Topology ist das Vorgehen so, dass man zuerst sich die aktuelle Topologie holt, diese dann klont, am Klon Anpassungen vornimmt und dann den Klon zur aktiven Topologie erklärt. Es ist ausserdem nötig, dass für das Vorgehen eine Index-Partition ohne Fehler vorhanden ist, das heisst, wenn nur eine vorhanden ist und diese einen Fehler hat, müsste man zuerst einen Index Reset machen. Die bestehende Index Component kann nicht entfernt werden, wenn sie die Letzte ist – das heisst, dass man zuerst in einem Durchgang eine neue Index Component hinzufügen muss, dann die Änderungen aktivieren muss und erst in einem zweiten Durchgang kann dann die problematische Komponente entfernt werden.

Hier der PowerShell-Code dazu:

# ----- Teil 1, neue Index-Komponente hinzufügen

# Aktive Topologie holen
$ssa = Get-SPEnterpriseSearchServiceApplication
$active = Get-SPEnterpriseSearchTopology -Active -SearchApplication $ssa 

# Einen Klon der aktiven Topologie erstellen
$clone = New-SPEnterpriseSearchTopology -SearchApplication $ssa -Clone -SearchTopology $active

# Search Service Instance des Servers "SERVER1" holen (Namen muss natürlich angepasst werden)
$hostServer1 = Get-SPEnterpriseSearchServiceInstance -Identity "SERVER1"

# Neue Index-Komponente erstellen und zum Klon hinzufügen, Ziel-Instanz auf den gewünschten Server konfigurieren
$newIndexComponent = New-SPEnterpriseSearchIndexComponent -SearchTopology $clone -SearchServiceInstance $hostServer1 -IndexPartition 0

# Verzeichnis für Index (auf der vorhin mitgegebenen Instanz (SearchServiceInstance)) einrichten
$newIndexComponent.RootDirectory = "I:\Index"

# Überprüfung der Konfiguration der Komponenten im Klon (muss nun die neue Komponente und auch die alte anzeigen)
Get-SPEnterpriseSearchComponent -SearchTopology $clone

# Den Klon als neue, aktive Topologie aktivieren (Änderungen werden erst jetzt aktiv!)
Set-SPEnterpriseSearchTopology -Identity $clone

# Überprüfung der aktiven Topologie
Get-SPEnterpriseSearchTopology -Active -SearchApplication $ssa

# ----- Teil 2, alte Index-Komponente entfernen

# Aktive Topologie neu holen
$active = Get-SPEnterpriseSearchTopology -Active -SearchApplication $ssa

# Einen Klon der aktiven Topologie erstellen
$clone = New-SPEnterpriseSearchTopology -SearchApplication $ssa -Clone -SearchTopology $active

# Search Components für die Topologie anzeigen (Component ID der degradierten Komponente kopieren)
Get-SPEnterpriseSearchComponent -SearchTopology $clone

# Index-Komponente (ID hier ersetzen/eintragen) aus der Topologie (Klon) entfernen
Remove-SPEnterpriseSearchComponent -Identity <Component ID> -SearchTopology $clone

# Den Klon als neue, aktive Topologie aktivieren
Set-SPEnterpriseSearchTopology -Identity $clone

# Überprüfung der aktiven Topologie
Get-SPEnterpriseSearchTopology -Active -SearchApplication $ssa

# ----- Teil 3, alte Topologie (da nun inaktiv) löschen (wird aktuell noch mit dem Namen '$active' referenziert)
Remove-SPEnterpriseSearchTopology -Identity $active<span style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" data-mce-type="bookmark" class="mce_SELRES_start"></span>

Nun sollte die Search Service Administration wieder eine saubere Topologie ohne Fehlermeldungen anzeigen.

Kategorien
IT SharePoint

SharePoint: Fehler beim Aktivieren einer Enterprise-Lizenz

Ich hatte letzthin einen Fall, wo ein Kunde von einer SharePoint (2013) Standard-Lizenz auf eine Enterprise-Lizenz wechseln wollte, da er neu einige der Enterprise-Features benötigte (z.B. für die Anzeige von Visio-Dateien).

Die Umstellung auf der Test-Farm funktionierte ohne Probleme. „Enable Enterprise Features“ in der „Central Administration“ gewählt, auf „Enterprise“ gewechselt und den Key eingegeben, nach einigen Sekunden Wartezeit kam die Erfolgsmeldung.

Doch als ich die selben Schritte auf der produktiven Farm durchführte, ging auf einmal nichts mehr. Die Websites waren nicht mehr verfügbar. Einige SharePoint-Services (z.B. der Timer-Service oder der Service für die Administration) waren gestoppt. Zur Sicherheit bootete ich die Server neu (es gab ja ein Wartungsfenster) und die Webseiten liefen wieder, allerdings war die Lizenz noch immer auf „Standard“.

Dann habe ich einen erneuten Versuch unternommen und es funktionierte leider wieder nicht. Zumindest brachte die Central Administration nun eine Fehlermeldung und sagte mir, dass ich in den Logs nachschauen sollte für mehr Details – auch stürzten die Webseiten nicht mehr ab.

In den ULS-Logs (zumindest auf dem Application Server) fand ich keine nützlichen Hinweise bezüglich der Fehlerursache. Ein Blick in den „Event Viewer“ zeigte mir einen Eintrag mit Source „SharePoint 2010 Products Configuration Wizard“ (auf einem „SharePoint 2013“-Server 🙂 ) mit der Mitteilung, dass der Wizard mit Erfolg durchgelaufen ist.

Da die Produktion aus 3 SharePoint-Servern bestand (1x App, 2x Web Frontend), ging ich via RDP auch auf die beiden WFE und öffnete auch dort den Event Viewer, auf dem ersten WFE gab es auch einen erfolgreichen „Product Configuration Wizard“-Eintrag, jedoch auf dem zweiten WFE gab es einen Fehler-Eintrag, mit folgendem Text:

„Failed to register SharePoint services.

An exception of type Microsoft.SharePoint.Administration.SPUpdatedConcurrencyException was thrown. Additional Information: An update conflict has occurred, and you must re-try this action. The object SearchAdminWebService was updated by Domain\AccountXy, in the PSCONFIG (9444) process on machine SERVER01. View the tracing log for more information about the conflict.

…“

Gemäss Fehlermeldung ist also ein Konflikt aufgetreten, es scheint so, als hätte ein Prozess auf dem einen SharePoint-Server den Prozess auf diesen Server gestört, da beide Prozesse etwas mit dem Objekt „SearchAdminWebService“ ändern wollten.

Die Suche im Internet lieferte zu diesem Problem leider nicht viele Lösungsvorschläge, jedoch ein Eintrag war vielversprechend. Ein Benutzer hatte eine Ähnliche Fehlermeldung (zumindest auch mit der Rückmeldung bezüglich „SPUpdatedConcurrencyException„) und er konnte das Problem lösen, in dem er die SharePoint-Timer-Services auf den SharePoint-Servern stoppte und dann (im Erfolgsfall) nach und nach aktivierte, so wurde verhindert, dass sich die Prozesse gegenseitig störten.

Ich habe also auf den beiden WFE-Servern den SharePoint-Timer-Service gestoppt. Dann habe ich auf dem App-Server den Lizenz-Key erneut eingegeben und auf OK geklickt. Sobald im Event-Viewer des App-Servers (Source: „SharePoint 2010 Products Configuration Wizard“) der Eintrag mit der Erfolgsmeldung für den „Product Configuration Wizard“ kam, habe ich auf dem ersten WFE-Server den Timerjob gestartet, als auch dort die Erfolgsmeldung kam, habe ich auf dem zweiten WFE den Timerjob gestartet und nach etwas Wartezeit kam dann auch dort die Erfolgsmeldung im „Event Viewer“.

Nach dem Zurückwechseln auf den App-Server wurde mir dort die Meldung präsentiert, dass die Lizenz erfolgreich eingespielt wurde und die CA fragte mich, ob ich auf den bestehenden Site-Collections das Enterprise-Feature aktivieren wollte – was ich verneinte.

Das Timing-Problem konnte also durch das serielle Starten der Timer-Jobs umgangen werden.

Kategorien
IT SharePoint

SharePoint: Formularfeld mittels JavaScript ermitteln

In SharePoint möchte man manchmal mittels eigenem JavaScript-Code ein Formularfeld (NewForm und EditForm) holen, zum Beispiel um dessen Wert auszulesen, um damit etwas zu machen oder man möchte via JavaScript einen solchen Feldwert auf einem Formular dynamisch setzen.

Es gibt leider kein schönes Bordmittel um den aktuellen Wert direkt aus dem Formular auszulesen (für den Wert aus einem Listitem natürlich schon), aber mit der folgenden Funktion erhaltet ihr mit der Eingabe des „internalFieldName“ (interner Feldname) die Tablerow (TR-Tag) des Feldes aus der Formular-Tabelle:

function getFieldTableRow(internalFieldName) {
	var row = null;
	jQuery("td.ms-formbody").each(function () {
		if (jQuery(this).html().indexOf("FieldInternalName=\"" + internalFieldName + "\"") != -1) {
			row = jQuery(this).closest("tr");
			/* break each loop */
			return false;
		}
	});
	return row;
}

Das schöne dabei ist, dass die Funktion auf den internen Feldnamen zurückgreift. Das heisst, das der Code auch bei Mehrsprachigkeit funktioniert oder falls einmal der Anzeigename eines Feldes geändert wird.

Möchte man nun z.B. die Zeile auf dem Formular ausblenden könnte man mittels jQuery folgendes tun:

var titleRow = getFieldTableRow("Title");
titleRow.hide();

Möchte man den Wert eines Feldes erhalten, könnte man folgendes tun:

var titleRow = getFieldTableRow("Title");
var titleValue = titleRow.find("input").val();

 

Kategorien
IT SharePoint SQL

Eine „suspect“ Datenbank reparieren

Es ist mir schon passiert, dass ich auf einmal keine SharePoint-Webseite meiner Virtual Machine mehr öffnen konnte. Auch die Central Administration funktionierte nicht mehr. Als Fehlermeldung erschien:

„This operation can be performed only on a computer that is joined to a server farm by users who have permissions in SQL Server to read from the configuration database. To connect this server to the server farm, use the SharePoint Products Configuration Wizard, located on the Start menu…“

Ein Ausführen des SharePoint Products Configuration Wizards war nicht möglich, der Wizard brach nach einigen Schritten ab, mit der Begründung, dass nicht auf die Configuration-Datenbank zugegriffen werden konnte.

Ein Blick in das MSSQL Management Studio zeigte dann, dass die SharePoint-Config-Datenbank im Status suspect, also verdächtig war.

Zum Glück gibt es einen Weg, die Datenbank wieder in einen gesunden Zustand zu bringen, oder zumindest es zu versuchen. Achtung: je nach Schweregrad der zu Grunde liegenden Störung kann es sein, dass bei diesem Vorgang Daten verloren gehen, da hier eine Reparatur der Datenbank mit „REPAIR_ALLOW_DATA_LOSS“ durchgeführt wird.

Via MSSQL Management Studio auf den Datenbankserver verbinden und ein neues Query-Fenster öffnen, danach folgende Zeilen ausführen („DatabaseName“ durch den Namen der „suspect“ Datenbank ersetzen):

EXEC sp_resetstatus [DatabaseName];
ALTER DATABASE [DatabaseName] SET EMERGENCY
DBCC checkdb([DatabaseName])
ALTER DATABASE [DatabaseName] SET SINGLE_USER WITH ROLLBACK IMMEDIATE
DBCC CheckDB ([DatabaseName], REPAIR_ALLOW_DATA_LOSS)
ALTER DATABASE [DatabaseName] SET MULTI_USER

Je nach Grösse der Datenbank, Server-Leistung und Fehler-Anzahl kann der Vorgang zwischen wenigen Sekunden bis hin zu Stunden dauern. Bei mir brauchte es für eine 1 GB grosse Datenbank zum Beispiel ca. eine Minute. Im Ausgabefenster ist ersichtlich, was für Reparaturen vorgenommen wurden und ob diese zu Datenverlust führten.

Nachdem die Operation beendet wurde, kann man versuchen wieder auf die Datenbank zuzugreifen. In meinem Fall konnte ich wieder auf die SharePoint-Central-Administration und die Web-Applications zugreifen.

Kategorien
IT SharePoint

Alle SharePoint-Solutions mit einem einzigen PowerShell-Befehl exportieren

Wenn man alle installierten Farm-Solutions/WSPs einer SharePoint-Farm mit einem simplen, einzeiligen PowerShell-Befehl exportieren möchte (vielleicht weil man sie sonst nirgends mehr hat oder nicht mehr sicher ist welche Version installiert ist – beides sollte man natürlich vermeiden), dann kann man in der SharePoint Management Shell einfach folgenden Befehl absetzen:

(Get-SPFarm).Solutions | ForEach-Object{$var = (Get-Location).Path + "\" + $_.Name; $_.SolutionFile.SaveAs($var)}

Die WSP-Datein befinden sich danach im aktuellen Working-Directory, welches man in der PowerShell „offen“ hatte, z.B. im Verzeichnis des aktuellen Benutzer-Kontos.

Kategorien
IT Python

QooliImapEmailExtractor – Ein IMAP Email-Adressen-Extrahierer in Python

Ich wurde kürzlich angefragt, ob ich nicht ein Programm kennen würde, um aus einen Mail-Postfach alle Email-Adressen zu extrahieren. Egal, ob diese im „To“- oder „From“- oder „CC“-Feld stehen.

Leider kannte ich kein Programm, dass dafür geeignet ist, obwohl es ein solches mit Sicherheit aber gibt. Aber bei vielen Gratis-Tools oder auch Trials kann man sich nicht sicher sein, ob es Malware enthält oder ob am Ende die Email-Adressen die extrahiert wurden auch noch gleich an den Hersteller des Programms gesendet werden.

Warum diese Aufgabe also nicht einfach selber mit Python lösen? Im Internet wurde ich schnell fündig und fand die Basis für ein Skript, welches schon einmal grundsätzlich alle Adressen aus einem einzelnen IMAP-Inbox-Folder eines Postfachs (mittels imaplib) in eine Variable extrahiert: https://www.quora.com/Is-there-a-way-to-extract-email-addresses-from-all-emails-in-my-Gmail-account/answer/Jerry-Neumann

Das Skript habe ich dann etwas schöner formatiert und erweitert, dass alle Folder eines Postfachs durchlaufen und die Daten dann in eine CSV-Datei exportiert werden. Das sieht dann wie folgt aus:

import imaplib
import email
import csv
import re


list_response_pattern = re.compile(r'\((?P<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)')


def parse_list_response(line):
    flags, delimiter, mailbox_name = list_response_pattern.match(line).groups()
    mailbox_name = mailbox_name.strip('"')
    return flags, delimiter, mailbox_name


def split_addresses(s):
    # split an address list into list of tuples of (name,address)
    if not s:
        return []
    out_q = True
    cut = -1
    res = []
    for i in range(len(s)):
        if s[i] == '"':
            out_q = not out_q
        if out_q and s[i] == ',':
            res.append(email.utils.parseaddr(s[cut + 1:i]))
            cut = i
    res.append(email.utils.parseaddr(s[cut + 1:i + 1]))
    return res


def main():
    print 'Processing started...'
    addresses = []
    mail = imaplib.IMAP4_SSL('imap.server.com')  # enter hostname eg: imap.gmail.com
    mail.login('user@server.com', 'Password')  # userid, password
    response_code_folders, mail_folder_lines = mail.list()
    for mail_folder_line in mail_folder_lines:
        flags, delimiter, mailbox_name = parse_list_response(mail_folder_line)
        print '  Extracting from folder: ' + mailbox_name
        mail.select(mailbox_name)
        result, data = mail.search(None, "ALL")
        ids = data[0].split()
        mail_count = len(ids)
        print '   Mails found in folder: ' + str(mail_count)
        if mail_count > 0:
            msgs = mail.fetch(','.join(ids), '(BODY.PEEK[HEADER])')[1][0::2]
            mail_ix = 0
            for x, msg in msgs:
                mail_ix += 1
                msgobj = email.message_from_string(msg)
                print '   extracting data from message ' + str(mail_ix) + '/' + str(len(msgs))
                addresses.extend(split_addresses(msgobj['to']))
                msgobj = email.message_from_string(msg)
                addresses.extend(split_addresses(msgobj['from']))
                addresses.extend(split_addresses(msgobj['cc']))
    # only take unique addresses
    addresses = set(addresses)
    csv_filename = 'email_addresses_export.csv'
    with open(csv_filename, 'wb') as out:
        csv_out = csv.writer(out)
        csv_out.writerow(['Name', 'Address'])
        for row in addresses:
            csv_out.writerow(row)
    print ' CSV with addresses written to: ' + csv_filename
    print 'Processing finished'


main()

Wenn ihr das Skript verwenden wollt, müsst ihr Python installiert haben und euren Mailserver und die Postfach-Logindaten eintragen:

mail = imaplib.IMAP4_SSL(‚imap.server.com‚)  # IMAP-Hostname
mail.login(‚user@server.com‚, ‚Password‚)  # Postfach-Benutzer-ID, Passwort