Stsadm vs Sharepoint Powershell cmdlet

Как известно, Microsoft активно продвигает Powershell, и старается использовать его в каждом своем продукте. Не исключением стала и очередная версия Sharepoint 2010. В данной статье я хотел бы поделиться своими соображениями на счет того, почему Powershell это наше всё, и почему следует активно отказывать от stsadm при работе с Sharepoint 2010.

С каждой последующей версией любой серьезный программный продукт обрастает функциональностью, и, как следствие, требует всё больше и больше системных ресурсов. В Sharepoint количество таких изменений и нововведений просто огромно. Поэтому требования к аппаратному обеспечению (особенно к памяти) выросли. Но этот вопрос должен больше волновать администраторов. Что же до разработчиков, то для них это вылилось в заметное удлинение процесса деплоя и отладки. stsadm – это .NET приложение, и ему свойственны все “болячки” таких приложений, связанные с производительностью. Только усугубляет ситуацию “модульность” stsadm.

Перейдем сразу к примеру. Думаю многим разработчикам, да и администраторам, знакома вот такая последовательность:

spsadm -o deactivatefeature -id 3bd1ede7-cfa3-45a7-b19e-70ae69a50db6 -url %TargetWebUrl% –force 
spsadm -o uninstallfeature -id 3bd1ede7-cfa3-45a7-b19e-70ae69a50db6 -force
echo Deactivating feature ...
spsadm -o deactivatefeature -id e240c347-7fd4-4ef8-9f57-424664e53e01 -url %TargetWebUrl% -force
echo Uninstalling feature ...
spsadm -o uninstallfeature -id e240c347-7fd4-4ef8-9f57-424664e53e01 -force
echo Retracting solution %PackageName% ...
spsadm -o retractsolution -name "%PackageName%" -local -url %TargetWebUrl%
echo Deleting solution %PackageName% from SharePoint ...
spsadm -o deletesolution -name "%PackageName%"

echo Adding solution %PackageName% to SharePoint ...
spsadm -o addsolution -filename "%PackageFile%"
echo Deploying solution %PackageName% ...
spsadm -o deploysolution -name "%PackageName%" -local -allowGacDeployment -url %TargetWebUrl%
echo Activating feature ...
spsadm -o activatefeature -id 3bd1ede7-cfa3-45a7-b19e-70ae69a50db6 -url %TargetWebUrl%
echo Activating feature ...
spsadm -o activatefeature -id e240c347-7fd4-4ef8-9f57-424664e53e01 -url %TargetWebUrl%

Стандартный скрипт для установки решения в Sharepoint, часто применяется для установки решений в продуктовой среде. Когда я запустил подобный скрипт для установки решения из нескольких рабочих процессов и пары-тройки фич на сервере под управлением Sharepoint 2010, то выполнение этого скрипта заняло у меня… 15 минут. Скрипт и сервер были те же самые, которые использовались для Sharepoint 2007, и на нем выполнение занимало 6-7 минут. Даже после добавления памяти на сервер время удалось уменьшить всего на 2-3 минуты.
 
Ждать столько времени желания не было никакого, особенно если учесть, что из Visual Studio такой же деплой идет за минуту-две. И тогда появилась идея попробовать переписать эту же последовательность на Powershell с применением командлет от Sharepoint 2010. Когда работа была закончена, результат превзошел все ожидания – вместо 12-15 минут те же самые действия в Powershell выполнялись за пару минут!!!! Кроме этого, благодаря Powershell, в скрипт были добавлены действия по открытию сайта (т.к. первое открытие занимает довольно ощутимое время), а также включение рабочих процессов в списках.
 
Для примера привожу тот же самый кусок скрипта, что и выше, но уже на Powershell:
Disable-SPFeature -Identity "3bd1ede7-cfa3-45a7-b19e-70ae69a50db6" -url $TargetWebUrl -Confirm:$false 
Uninstall-SPFeature -Identity "3bd1ede7-cfa3-45a7-b19e-70ae69a50db6" -Confirm:$false
write-host Deactivating feature ...
Disable-SPFeature -Identity "e240c347-7fd4-4ef8-9f57-424664e53e01" -url $TargetWebUrl -Confirm:$false 
write-host Uninstalling feature ...
Uninstall-SPFeature -Identity "e240c347-7fd4-4ef8-9f57-424664e53e01" -Confirm:$false 
write-host Retracting solution $PackageName ...
Uninstall-SPSolution -Identity $PackageName -local -Confirm:$false 
write-host Deleting solution $PackageName from SharePoint ...
Remove-SPSolution -Identity $PackageName  -Confirm:$false
 
write-host Adding solution $PackageName to SharePoint ...
Add-SPSolution -LiteralPath $PackageFile -Confirm:$false
write-host Deploying solution $PackageName ...
Install-SPSolution -Identity $PackageName  -local -GACDeployment -Confirm:$false
write-host Activating feature ...
Enable-SPFeature -Identity "3bd1ede7-cfa3-45a7-b19e-70ae69a50db6" -url $TargetWebUrl -Confirm:$false
write-host Activating feature ...
Enable-SPFeature -Identity "e240c347-7fd4-4ef8-9f57-424664e53e01" -url $TargetWebUrl -Confirm:$false
 
Надеюсь эта информация поможет вам принять решения, и начать использовать Powershell в своей повседневной работе.
 
PS Это мой последний про Sharepoint. В связи со сменой работы, у меня несколько сместился акцент деятельность. Теперь я буду плотно заниматься продуктами из линейки System Center, в частности Operations Manager, Configurations Manager и Service Manager.

PowerEventReceivers в Sharepoint 2010

Ранее я писал о замечательном решении PowerEventReceivers. Если же вы захотите использовать его в Sharepoint 2010, вы с толкнетесь с проблемой – при открытии формы редактирования текста скрипта вы получите сообщение “Возникла неожиданная ошибка”, в логе другое сообщение: “Access Denied! Current user is not a farm administrator.”

Дело в том, что данное решение проверяет принадлежность пользователя к группе администраторов фермы с помощью системного вызова “SPFarm.Local.CurrentUserIsAdministrator()”. Но в Sharepoint 2010 этот вызов изменился, в функцию добавилась переменная allowContentApplicationAccess типа bool. Именно её надо выставить в true, чтобы правильно определялась принадлежность к группе из веб-страниц.

Перекомпилированную версию для Sharepoint 2010 вы можете скачать здесь.

Замечание: данная версия пока что не позволяет использовать новые командлеты Sharepoint 2010.

Sharepoint и FBA: создание пользователей из PowerShell. Часть 2

Этап второй. Включение пользователя в Sharepoint.

На этом “грабли” FBA не заканчиваются, а только начинаются. Для того. чтобы включить пользователя в Sharepoint, необходимо выполнить два действия:

  1. Вызвать метод класса SPWeb EnsureUser(string login)
  2. Добавить пользователя в какую-нибудь группу.

Проблемы у вас начнутся, как только вы попытаетесь подключиться к сайту Sharepoint с включенным FBA — API не обладает средствами авторизации для работы с Sharepoint с помощью FBA. И если вы просто подключитесь к сайту и вызовите метод EnsureUser, вы получите ошибку “Exception calling "EnsureUser" with "1" argument(s): "Attempted to perform an unauthorized operation."”. И вот тут начинается магия .NET и API Sharepoint. Решение с HttpContext я честно подсмотрел в инете. Подключение к сайту Sharepoint с включенным FBA будет работать вот так и только так:

   1: [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Sharepoint") | Out-Null
   2:  
   3: [Microsoft.Sharepoint.SPSecurity]::RunWithElevatedPrivileges({ 
   4:  
   5: [Microsoft.Sharepoint.SPSite]$site = new-object Microsoft.Sharepoint.SPSite("http://fbasite")
   6: [Microsoft.Sharepoint.SPWeb]$web = $site.RootWeb
   7: if($web)
   8: {
   9:     $web.AllowUnsafeUpdates = $True
  10:     if (![System.Web.HttpContext]::Current)
  11:     {
  12:         $request = new-object System.Web.HttpRequest("", $web.Url, "");
  13:         $writer = new-object System.IO.StringWriter
  14:         $response = new-object System.Web.HttpResponse($writer)
  15:         [System.Web.HttpContext]::Current = new-object System.Web.HttpContext($request, $response);
  16:         [System.Web.HttpContext]::Current.Items["HttpHandlerSPWeb"] = $web;
  17:     }
  18:  
  19:     [string]$fullName = "Иванов иван Иванович"
  20:     [string]$username = "CustomSQLMembershipProvider:TestLogin"
  21:         
  22:     [Microsoft.Sharepoint.SPUser]$curUser = $web.EnsureUser($username);
  23:  
  24:     if ($curUser)
  25:     {
  26:        $curUser.Name = $fullName;
  27:        $curUser.Update();
  28:        $web.SiteGroups["Посетители"].AddUser($curUser);
  29:     }
  30:     
  31:     $web.AllowUnsafeUpdates = $False
  32:     
  33:     $web.Dispose()
  34:     $site.Dispose()
  35: }
  36: })

Обратите внимание на строки 10-17 – тут  мы создаем текущий контекст для работы с FBA, иначе вызов метода UnsureUser вызовет ошибку. Кроме этого, весь код работы с API Sharepoint выполнен внутри метода  RunWithElevatedPrivileges – это необходимо даже в том случае, если вы работаете из под учетной записи администратора фермы Sharepoint. Кроме того, обратите внимание в каком виде необходимо передавать логин методу UnsureUser – с обязательным указанием имени провайдера.

Замечание: Еще одним способом является создание альтернативного узла IIS, который затем будет включен в SharePoint и настроен на тот же сайт, что и узел с FBA. В этом случае вы можете подключаться к этому альтернативному узлу по средствам Windows-аутентификации, но такая настройка мягко скажем не тривиальна.

В итоге у меня получился вот такой скриптик, который выполняет все необходимые действия по добавлению пользователей в Sharepoint с включенным FBA:

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Sharepoint") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("System.Web") | Out-Null
#====================================================================================
function Get-MembershipProvider([string]$ProviderName)
{
if($ProviderName)
{
#Находим провайдер по имени
return [System.Web.Security.Membership]::Providers[$ProviderName]
}
else
{
#Если имя не было указано, возвращаем провайдер по умолчанию
return [System.Web.Security.Membership]::Provider
}
}
#====================================================================================
function Create-ProviderUser($login, $password, $mail)
{
#проверяем, "подан ли на вход" провайдер
$provider = $input | select -First 1
if($provider -isnot [System.Web.Security.MembershipProvider])
{
$provider = Get-MembershipProvider
}

$status = 0
$question = "q"
$answer = "a"
$approved = $True
$retStatus = 0
$newUser = $provider.CreateUser($login, $password, $mail, $question, $answer, $approved, $null, [ref]$retStatus)
write-host "Статус создания пользвоателя а MembershipProvider: $retStatus"

return [System.Web.Security.MembershipUser]$newUser
}
#====================================================================================
function Create-SharepointUser([string]$SiteUrl,[System.Web.Security.MembershipUser]$User, [string]$FullName, [string]$GroupName)
{
[Microsoft.Sharepoint.SPSecurity]::RunWithElevatedPrivileges({
[Microsoft.Sharepoint.SPSite]$site = new-object Microsoft.Sharepoint.SPSite($SiteUrl)
[Microsoft.Sharepoint.SPWeb]$web = $site.RootWeb
if($web)
{
$web.AllowUnsafeUpdates = $True
if (![System.Web.HttpContext]::Current)
{
$request = new-object System.Web.HttpRequest("", $web.Url, "");
$writer = new-object System.IO.StringWriter
$response = new-object System.Web.HttpResponse($writer)
[System.Web.HttpContext]::Current = new-object System.Web.HttpContext($request, $response);
[System.Web.HttpContext]::Current.Items["HttpHandlerSPWeb"] = $web;
}

[Microsoft.Sharepoint.SPUser]$curUser = $web.EnsureUser($User.UserName);
if ($curUser -is [Microsoft.Sharepoint.SPUser])
{
$curUser.Name = $FullName
#После вызова метода Update может выскочить ошибка "Unable to cast object of type 'System.Management.Automation.PSObject' to type 'Microsoft.SharePoint.SPWeb'."
#При этом всё работает. Причину и решение я не нашел.
try{ $curUser.Update();}catch{}
[Microsoft.Sharepoint.SPGroup]$group = $web.SiteGroups[$GroupName]
$group.AddUser($curUser)
}

$web.AllowUnsafeUpdates = $False

$web.Dispose()
$site.Dispose()
}
})
}
#====================================================================================

#собственно код создания
$provider = Get-MembershipProvider "AtlantisSQLMembershipProvider"
$login = "TestLogin2"
$password = "!@#_s123QWE"
$email = "my@mail.local"
$user = $provider | Create-ProviderUser $login $password $email

if($user)
{
Create-SharepointUser http://fbasite $user "Иванов Иван Иванович" "Посетители"
}

Sharepoint и FBA: создание пользователей из PowerShell. Часть 1

Я уже давно работаю с Sharepoint, приличное время с PowerShell. И вот недавно мне поставили задачу – перевести функционал сайта, сделанного в 1С:Битрикс на WSS. Всё было просто, пока мы не дошли до стадии создания логинов на сайте.

Т.к. сайт предназначался для доступа из вне, естественным было использовать FBA в Sharepoint – Form Based Authentication. Встала необходимость создать порядка 50 пользователей, список которых был выгружен из 1С:Битрикс. Создание пользователей для Sharepoint  в обычной среде (с доменом и авторизацией в AD)  проблемой не является. Но, как оказалось, с FBA всё сильно сложнее.

Я не буду останавливаться на процессе собственно включения FBA в Sharepoint, благо про это написано не одна статья. В итоге у нас должен появиться свой собственный провайдер аутентификации и RoleManagement-а. Итак, процесс создания пользователя в общих чертах выглядит следующих образом:

  1. Создать пользователя в нашем провайдере аутентификации. Для этого нам надо указать логин, пароль, и электронную почту (в зависимости от настроек провайдера может потребоваться также подстановка контрольного вопроса и ответа). После этого пользователь будет создан в БД.
  2. Включить пользователя в Sharepoint, и добавить его в необходимую группу.

Этап первый.

Для создания пользователей в провайдере используется пространство имен [System.Web.Security]. В данном пространстве есть класс [System.Web.Security.MembershipProvider], экземпляры которого содержит в себе несколько методов для работы с пользователями, в том числе метод CreateUser, который нам и нужен.

Собственно код для создания пользователя в провайдере достаточно прост:

   1: [System.Reflection.Assembly]::LoadWithPartialName("System.Web")  | Out-Null
   2: $provider = [System.Web.Security.Membership]::Providers["CustomSqlMembershipProvider"]
   3: $login = "TestLogin"
   4: $password = "Secure1_Password"
   5: $mail = "test@mail.local"
   6: $question = "q"
   7: $answer = "a"
   8: $approved = $true
   9: $provider.CreateUser($login, $password, $mail, $question, $answer, $approved, $null, [ref]$status)

На вид – ничего сложного. Но когда вы запустите этот код, ваш созданный ранее провайдер не будет найден. Дело всё в том, что пространство имен [System.Web.Security] рассчитано на то, что все его классы будут использоваться внутри веб-сервера, и как следствие иметь доступ к настройках текущего узла. Точно также работает и класс [System.Web.Security.MembershipProvider] – вызов свойства [System.Web.Security.MembershipProvider]::Providers в свою очередь вызывает внутренние методы, которые обращаются к файлу настроек веб-сервера (а точнее, что важно как вы увидите потом – к файлу настроек приложения веб-вервера). Все настройки веб-севрера IIS хранятся в файле c:\Windows\Microsoft.NET\Framework64\v2.0.50727\CONFIG\web.config. Там же, в секции <configuration><roleManager> хранится и созданный провайдер. Логично, PowerShell вообще ничего не знает об этом файле настроек и не умеет его использовать.

Вот тут проявляется вся сила и простота .NET Framework. Всё, что нам нужно сделать – создать файл настроек (.config) для PowerShell. Для этого достаточно каталоге с файлом powershell.exe (или powershell_ise.exe, в зависимости от того, чем вы пользуетесь) создать одноименный файл powershell.config и\или powershell_ise.config со следующим шаблоном:

   1: <?xml version ="1.0"?>
   2: <configuration>
   3:   <system.web>
   4:   </system.web>
   5: </configuration>

После этого внутрь секции syste.web скопировать секции <roleManager> и <membership> из web.config, а также вставить после секции </system.web> секцию <connectionStrings> из того же web.config. В итоге у вас должно получиться примерно следующее:

   1: <?xml version ="1.0"?>
   2: <configuration>
   3:  <system.web>
   4:   <roleManager>
   5:    <providers>
   6:     <add name="CustomSqlRoleprovider" type="System.Web.Security.SqlRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" applicationName="/" connectionStringName="LocalSqlServer" />
   7:    </providers>
   8:   </roleManager>
   9:   <membership>
  10:     <providers>
  11:       <add name="CustomSQLMembershipProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" applicationName="/" connectionStringName="LocalSqlServer" enablePasswordReset="true" enablePasswordRetrieval="false" passwordFormat="Hashed" requiresQuestionAndAnswer="false" requiresUniqueEmail="true" minRequiredNonalphanumericCharacters="1" minRequiredPasswordLength="8" />
  12:     </providers>
  13:   </membership>
  14:  </system.web>
  15:  <connectionStrings>
  16:    <remove name="LocalSqlServer" />
  17:    <add connectionString="Server=localhost;Database=FormAuthDB;Integrated Security=true" name="LocalSqlServer" providerName="System.Data.SqlClient" />
  18:  </connectionStrings>
  19: </configuration>

После этого, если вы запустите тот же самый скрипт – он должен отработать корректно.

Powershell v2 и Sharepoint: скриптописание или программирование для администраторов. Часть 2.

В предыдущей статье мы рассмотрели общие аспекты использования Powershell и Sharepoint, сейчас мы подробно разберем события элементов списков и события списков.

ListItemEventReceivers и ListEventReceivers

Для всех библиотек и списков в Sharepoint предусмотрен ряд обработчиков событий, часть которых отвечают за события непосредственно списка (изменение свойств, добавление столбцов и т.д.), а часть за события элементов (создание, изменение, удалении и т.д.).

В общем случае, для того, чтобы обрабатывать эти события необходимо создать класс, наследованный от SPItemEventProperties, и реализовать в нем нужные методы. После этого класс необходимо зарегистрировать в Sharepoint с помощью Feature или с помощью сторонних инструментов. Но т.к. C#, компиляция и прочие страшные слова – это не то, что нам сейчас нужно, мы посмотрим что же можно сделать с этими событиями с помощью Powershell.

На помощь нам придет замечательная Feature PowerEventReceivers из пакета iLoveSharepoint. Данная Feature добавляет в каждый список или библиотеку документов возможность вставлять обработчики событий на скриптах Powershell.

Тут следует отвлечься, и немного рассказать о некоторых аспектах безопасности, связанных с событиями. Все обработчики событий в списках и элементах списков выполняются от имени того пользователя, который инициировал изменения. В связи с этим и скрипты Powershell будут выполняться от имени этих пользователей, а пользователю необходимы права на выполнения скриптов на сервере, где установлена front-end MOSS\WSS. В противном случае вы получите ошибку “Requested registry access is not allowed”. Решить эту проблему можно, используя метод SPSecurity.RunWithElevatedPrivileges в WSS\Sahrepoint. Благодаря ему все вызовы будут происходить с привелегиями Системной учетной записи (от которой запущен пул приложений IIS). Но при этом нужно понимать, что ВСЕ скрипты в обработчиках будут запускаться от имени этой учетной записи, и с помощью неё можно наделать не мало бед. Подробно аспекты безопасности будут рассмотрены в конце цикла статей.

Какой из методов использовать – решать вам. Я создал PowerEventReceivers, который выполняется с повышенными привилегиями. Вы можете использовать его или оригинальный.

Установка крайне проста – запустите deploy.bat на сервере Sharepoint, затем активируйте эту Feature на нужном узле (и только на нужном, по соображениям безопасности и производительности):

image

После активации будут доступны два новых пункта в свойствах списков и библиотеках документов:

image

Смысл их понятен из названия, первый отвечает на обработку событий элемента, второй – за обработку событий списка (или библиотеки). Приведу полностью доступные обработчики элемента списка:

ItemAdding Во время добавления элемента
ItemUpdating Во время изменения элемента
ItemDeleting Во время удаления элемента
ItemCheckingIn Во время возврата документа
ItemCheckingOut Во время извлечения документа
ItemUncheckingOut Во время отмены извлечения документа
ItemAttachmentAdding Во время добавления вложения
ItemAttachmentDeleting Во время удаления вложения
ItemFileMoving Во время перемещения файла
ItemAdded После добавления элемента
ItemUpdated После изменения элемента
ItemDeleted После удаления элемента
ItemCheckedIn После возврата документа
ItemCheckedOut После извлечения документа
ItemUncheckedOut После отмены извлечения документа
ItemAttachmentAdded После добавления вложения
ItemAttachmentDeleted После удаления вложения
ItemFileMoved После перемещения файла
ItemFileConverted После конвертации файла (MOSS)
ContextEvent “Ручной” вызов события

и события списка (библиотеки):

FieldAdded После добавления столбца
FieldAdding Во время добавления столбца
FieldDeleted После удаления столбца
FieldDeleting Во время удаления столбца
FieldUpdated После изменения столбца
FieldUpdating Во время изменения столбца

Все обработчики с постфиксом –ing являются синхронными, то есть выполняются одновременно с действием, их породивших. Это позволяет влиять на событие вплоть до его отмены.

Для редактирования доступно как обычное текстовое поле прямо на веб-странице, так и специальный ActiveX для интеграции PowerEventreceivers с редактором PowerGUI. ActiveX доступен на официальном сайте.

Давайте посмотрим, что мы можем сделать с помощью этих обработчиков событий и Powershell (также несколько примеров доступно на страничке PowerEventReceivers).

1. Проверка значений перед вставкой элемента:

    1 function ItemAdding

    2 {

    3     [int]$curVal = $properties.AfterProperties["Title"]

    4     if ($curVal -ge 8)

    5     {

    6         $properties.Cancel = $true

    7         $properties.ErrorMessage = "Нельзя указывать значения поля Название больше или равное 8. Вы указали $curVal"

    8     }

    9 }

Обратите внимание на приведения типов в строке 3. Столбец Название (Title) на самом деле имеет тип “строка”, но это Powershell. Тут мы можем делать и вот так.

Если пользователь ввел значение больше или равное 8, то элемент не будет создан, а ему будет выдана вот такая красивая ошибка:

image

2. Оценка поля до изменения и после, вставка разницы в другой столбец. Мы будем использовать ItemUpdating для получения изменений, т.к. только в нем доступны как свойства до изменения, так и после. Кроме этого мы будем использовать ItemUpdated для фиксации изменений в поле, т.к. вызов метода Update в ItemUpdating приведет к конфликту сохранений:

    1 function ItemUpdating

    2 {

    3     $oldVal = $item["Title"]

    4     $newVal = $properties.AfterProperties["Title"]

    5     $val = "Было $oldVal, а стало $newVal"

    6     $this.DisableEventFiring()

    7     $item.Properties["_Temp"]  = $val

    8     $item.SystemUpdate($False)

    9     $this.EnableEventFiring()

   10 

   11 }

   12 

   13 function ItemUpdated

   14 {

   15     $this.DisableEventFiring()

   16     $item["Field2"] = $item.Properties["_Temp"]

   17     $item.SystemUpdate($False)

   18     $this.EnableEventFiring()

   19 }

Обратите внимание на строки 6, 9, 15 и 18. Методы DisableEventFiring и EnableEventFiring позволяют отключить обработчики событий на время, чтобы у нас не было циклического из вызова из-за изменений в коде.

Также интересна конструкция $item.Properties["_Temp"]  = $val. Свойство Properties позволяет нам записывать любые значения в элемент, которые мы затем можем получить. В основном данное свойство применяется в рабочих процессах (Workflow), но как видно может пригодиться и тут.

Вот результат работы скрипта:

image

3. Создание сложных вычисляемых полей. Данный пример – следствие предыдущего. Формулы в Sharepoint хоть и обширны, но им очень далеко до функционала Powershell. Здесь мы можем применять любые функции, доступные нам в Powershell (и .NET соответственно) для создания вычисляемых полей:

    1 function GetListFromLookupField([string]$InternalFieldName)

    2 {

    3 

    4     $typeField = [Microsoft.SharePoint.SPFieldLookup]$list.Fields.GetFieldByInternalName($InternalFieldName)

    5     $typeListGuid = newobject Guid($typeField.LookupList)

    6     $typeList = $web.Lists[$typeListGuid]

    7     return $typeList

    8 }

    9 

   10 function ItemUpdated

   11 {

   12     $lookupList = GetListFromLookupField("LookupField")

   13 

   14     $this.DisableEventFiring()

   15     $item["LinkField"] = $site.Url + $lookupList.DefaultViewUrl+", Ссылка на список"

   16     $item.SystemUpdate($False)

   17     $this.EnableEventFiring()

   18 }

У нас в списке 2 поля. LookupField – поле типа “Подстановка”, LinkField – поле типа “Гиперссылка”. В результате выполнения в поле LinkField будет записана ссылка на представление по умолчанию списка. из которого берутся данные в поле LookupField. Результат выполнения:

image

Думаю из этих примеров понятно, что обработка событий предлагает нам неограниченные средства по работе с Sharepoint – дело только в фантазии.

Отладка.

Хотелось бы сказать пару слов об отладке. Первое, что отмечу – все ошибки выполнения PowerEventReceivers записывают в системный журнал Приложения. Т.е. если у вас что-то не выполнилось – смотрите журнал:

image

Для отладки же и получения значений каких-либо переменных я использую следующую конструкцию:

    1 ($item["Field"] | ConvertTo-XML).Save("d:\FieldValue_"+[datetime]::Now.ToString("HH.mm.ss.FFFFF")+".xml")

Т.е. просто преобразую значение переменной в XML, а затем записываю на диск в виде XML-файла с меткой времени.

Sharepoint и PowerShell – скриптописание или программирование для администраторов. Часть 1.

Вступление

image Когда речь заходит о Sharepoint и расширении его функционала за счет создание рабочих процессов, обработки событий и прочего, многие администраторы сразу отмахиваются от всего этого с формулировкой “Уууу… Это же программировать надо!”. При этом эти же администраторы пишут скрипт на VBScript,JScript и Powershell, правда называя это не программированием, а скриптописанием. Так им проще. И понятия C#, C++ и т.д. вызывают священный трепет или откровенную боязнь. Меня всегда несколько удивляло. Возможно, мне в этом плане повезло больше – свою деятельность в ИТ я начинал именно с программирования, и лишь намного позже занялся администрированием.

Несколько лет тому назад я начал изучать язык Lua, особенностью которого было то, что этот на 100% скриптовый язык легко можно “встроить” в любое приложение. Т.е. при запуске откомпилированного приложения будет происходить обращение к тексту скрипта, который лежит в открытом виде и может быть легко отредактирован конечными пользователями. Примерно тогда же появился и PowerShell, который, с некоторыми оговорками, тоже умел такое делать. Но это было давно, и тогда главным отличием для меня между Lua и Powershell была производительность – выполнение скрипта в Lua отличалось от выполнения полностью откомпилированного кода на десятые доли процента (с учетом, что речь идет о С++ и его компиляторе, это действительно впечатляло). О производительности PowerShelll, думаю, лучше не рассуждать. Именно тогда родилась идея использовать скрипты внутри Sharepoint.

Собственно многие, кто занимается разработкой для Sharepoint, могут сказать “Зачем такой изврат?”. Аргументов несколько:

  • Как я уже писал – доступность скриптовых языков для администраторов
  • Отсутствие необходимости делать деплой проектов для серверах Sharepoint. Что очень помогает – больше не нужно перегружать IIS или пул приложений, меньше простоев – больше довольных пользователей.
  • Отсутствие версий библиотек. С одной стороны это не всегда удобно, с другой — просто меньше головной боли.
  • Возможность быстрого внесения изменений в функционал

И это далеко не всё. Единственное, что меня тогда остановило – отсутствие вменяемой библиотеки для работы Lua с .NET. Была библиотека LuaInterface, но её реализация “хромала” из-за использования не безопасных вызовов (читай “чистых” С-шных методов). Правда позже появилась версия LuaInterface за номер два, которая была полностью переписала на CLI, и она уже работала стабильно, но к этому моменту у меня в Sharepoint всё было написано на чистом C#, поэтому интеграция Lua и Sharepoint так и не состоялась.

С тех пор прошло немало времени, вышел PowerShellv2. И вот когда в очередной раз мне пришлось писать проект для Sharepoint, я вспомнил о своей давней идее. И опять выбор стоял между Lua и PowerShell. И выиграл PowerShell, причин тому много:

  • Адаптация кода. Синтаксис PS и С# во многом схож, поэтому адаптировать уже написанный код гораздо проще. Lua похож на C, поэтому фактически пришлось бы переписывать всё заново.
  • Поддержка .NET. Всё-таки .NET и PS это близнецы-братья, поэтому тут даже рассуждать нечего.
  • Поддержка Sharepoint. К моменту запуска проекта была создано большое кол-во сторонних средств для интеграции PS и Sharepoint.
  • Производительность. Lua конечно быстрее, но Sharepoint сам по себе мягко скажем не система реал-тайм обработки данных, поэтому я бы всё равно уперся в производительность .NET

Перед тем, как перейти к практической реализации, давайте подумаем, что обычно приходится программировать для Sharepoint. Это будут на 100% только мои рассуждения, у каждого может быть свой взгляд на этот вопрос:

  • Workflow (Рабочие процессы). Это наиболее часто используемые элементы. К сожалению, Sharepoint Designer не всегда позволяет реализовать все задачи (н-р он просто не умеет делать State Machine worflow). Но РП – не самые лучшие кандидаты на скриптование в силу их архитектуры. Мы можем конечно использовать частично PowerShell, но он компиляции нас это не спасет.
  • WebPart (веб-части) – компоненты страниц, которые позволяют отбражать на странице некую информацию. В этом аспекте, как говориться, всё сделано до меня. Есть масса реализация веб-частей, которые могут выполнять скирпты PowerShell
  • List and ListItems Event Receivers (События списков и элементов списков). Крайне полезный, но недооцененный инструмент. Для PowerShell есть уже замечательное готовое решение iLoveSharepoint SharePoint PowerEventReceivers, о котором я напишу чуть позже.
  • Timer Jobs (Задания таймера). Если вам нужно запускать операции в Sharepoint с какой-то периодичностью – это тот инструмент, который вам нужен. К тому же он – первый кандидат на скриптование

Сразу оговорюсь, что в статье я буду рассматривать только вызовы скриптов из самого Sharepoint. Про скрипты для Sharepoint, которые запускаются конечным пользователем, в интернете есть масса материала. Еще одно замечание для больших любителей и знатоков PowerShell – представленный код в статье носит скорее ознакомительный характер, и написан человеком, который много лет изучает C#. Соответственно там, где можно использовать конструкции C# вместо аналогов из PowerShell, будут использованы версии C#. Например я пишу [dateTime].Now вместо Get-Date, foreach() вместо | % {} и т.д.. На логику скрипта это никак не влияет. Также, если кто-то считает, что определенные конструкции можно заменить на более короткие или быстрые – не стесняемся, предлагаем.

Timer Jobs и PowerShell

Timer Jobs – основа основ в Sharepoint. Если не работает служба таймера – не работает добрая половина функционала Sharepoint. Фактически же Timer Job – это система планировщика, шедулера. Есть задачи, у каждой задачи есть расписание. Реализовать собственный Timer Job очень просто – достаточно написать класс, наследованный от SPJobDefinition, переопределить метод Execute и затем зарегистрировать его в Sharepoint (например с помощью TimerJobManager или скриптом).

Для того, чтобы выполнять скрипты PowerShell, нам необходимо:

  1. Создать класс-шаблон для запуска скриптов PowerShell
  2. Написать нужный скрипт
  3. Создать нужное кол-во задач в Sharepoint на базе нашего класса с помощью TimerJobManager, передав как параметр нужный скрипт

Для того, чтобы создать простейший класс вам потребуется (хотя вы можете этого не делать – я выложил готовый класс для использования):

  1. Установить Visual Studio 2008 (любая редакция, даже Express)
  2. Установить PowerShell V2 (можно конечно и V1, но зачем)
  3. Установить Powershell SDK V2 (не обращайте внимание на название файла – это не только примеры, но и сам SDK)
  4. Создать проект в VS2008 Class Library, используя .NET версии 3.5
  5. Добавить ссылки (Add reference) на “C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\ISAPI\Microsoft.SharePoint.dll” и “C:\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0\System.Management.Automation.dll”
  6. Наследовать класс от Microsoft.SharePoint.Administration.SPJobDefinition
  7. Реализовать конструкторы и метод Exeсute
  8. Подписать проект, чтобы у него было Strong name (в свойствах проекта – закладка Singing, чекбокс Sign the assembly)

Подробнее о создании Timer Job можно почитать в MSDN.

Приведу код моего класса полностью:

    1 using System;

    2 using System.Collections.Generic;

    3 using System.Linq;

    4 using System.Text;

    5 using Microsoft.SharePoint;

    6 using Microsoft.SharePoint.Administration;

    7 using System.Management.Automation.Runspaces;

    8 using System.IO;

    9 

   10 namespace SPPowerShellJob

   11 {

   12     public class SPPowerShellJob : SPJobDefinition

   13     {

   14         /// <summary>

   15         /// Текст скрипта

   16         /// </summary>

   17         [Persisted]

   18         public string ScriptText = "";

   19         /// <summary>

   20         /// Локальный путь на сервере (полный) к скрипту

   21         /// </summary>

   22         [Persisted]

   23         public string ScriptLocalPath = "";

   24 

   25         private const string JobName = "PowerShell Job";

   26 

   27         #region Constructors

   28         public SPPowerShellJob() : base() {

   29 

   30         }

   31 

   32         public SPPowerShellJob(SPWebApplication webApp)

   33             : base(JobName, webApp, null, SPJobLockType.Job)

   34         {

   35             this.Title = JobName;

   36         }

   37 

   38         public SPPowerShellJob(string name,

   39                             SPWebApplication webApplication,

   40                             SPServer server,

   41                             SPJobLockType lockType)

   42             : base(name, webApplication, server, lockType)

   43         {

   44             this.Title = name;

   45         }

   46         #endregion

   47 

   48         public override void Execute(Guid targetInstanceId)

   49         {

   50             string _scriptText = "";

   51 

   52             if (String.IsNullOrEmpty(ScriptText))

   53             {

   54                 //тело скрипта не указано, так что пытаемся считать его из файла

   55                 //Я не делаю обработку исключений, т.к. эти может заниматься и сам Sharepoint

   56                 //По хорошему конечно надо бы её делать

   57                 FileInfo scriptFile = new FileInfo(this.ScriptLocalPath);

   58                 StreamReader reader = scriptFile.OpenText();

   59                 _scriptText = reader.ReadToEnd();

   60                 reader.Close();

   61             }

   62             else

   63             {

   64                 _scriptText = this.ScriptText;

   65             }

   66 

   67             Runspace runspace = null;

   68             try

   69             {

   70                 //Стандартная инициализация PowerShell

   71                 runspace = RunspaceFactory.CreateRunspace();

   72                 runspace.Open();

   73                 Pipeline pipeline = runspace.CreatePipeline();

   74                 runspace.SessionStateProxy.SetVariable("this", this);

   75                 pipeline.Commands.AddScript(_scriptText);

   76 

   77                 //Обычно Invoke вызывают, чтобы получить выходные данные

   78                 //но нам это совсем не нужно — у нас вся логика в скрипте

   79                 //Так что просто запускаем скипт

   80                 pipeline.Invoke();

   81 

   82             }

   83             catch (Exception er)

   84             {

   85                 //Тут по желанию можно сделать обработку событий.

   86                 //Н-р запись в журнал системы или (для MOSS) в PortalLog.LogString()

   87                 throw new Exception("Error in SPPowerShellJob: " + er.Message, er);

   88             }

   89             finally

   90             {

   91                 if (runspace != null)

   92                 {

   93                     runspace.Close();

   94                     runspace.Dispose();

   95                 }

   96             }

   97         }

   98     }

   99 }

Задание имеет два параметра – ScriptText и ScriptPath. Подразумевается, если указан текст скрипта, то выполнять надо его. Если текст не указан – читать скрипт с диска.

Собственно самое интересное, касаемо PowerShell происходит в строках 71-80. Мы создаем Runspace (окружение), открываем, создаем поток команд, добавляем в поток наш скрипт и запускаем на выполнение. Функция Invoke() возвращает значение, которые возвращает ей скрипт, но в нашем случае использовать их негде. Обратите внимание – мы в окружение передаем переменную this, которую затем можно использовать в скрипте ($this).

Собственно вот и всё. Наш класс готов, можно добавлять его в Sharepoint. Для этого необходимо добавить библиотеку в GAC (в Проводнике перетащите библиотеку в папку C:Windows\assembly), после чего установите TimerJobManager, перейдите в Центр Администрирования –> Операции (1) –> Manage Timer Jobs, выберите представление “Веб-приложение” (2), выберите нужное приложение (3) и нажмите нажмите Add Job (4):

image

Вам будет предложено ввести:

  • Полное имя класса, которое включает в себя Namespace.Class, AssemblyName, Vesrion, Culture, PublicyToken. Н-р для моего класса это SPPowerShellJob.SPPowerShellJob, SPPowerShellJob, Version=1.0.0.0, Culture=neutral, PublicKeyToken=98bb5bf49f9d34cb.
  • Имя задачи (которое будет отображаться в списке задач). Вводите любое понятное, но необходимо что бы оно было уникальным
  • Тип блокировки (обычно это Job, см. документацию)

image

После этого нажмите Next, и, если всё в порядке, вы увидите окно с расписанием и параметрами задачи:

image

Добавите текст скрипта или путь (локальный относительно сервера, н-р c:\Scripts\TestScript.ps1), задайте нужное расписание и сохраните скрипт. Вот собственно и все.

Теперь немного о написании скриптов. Для примера приведу код, который всегда приводится, когда речь идет о заданиях таймера – код обхода веб-приложения и открытия каждого узла (это делает для того, что бы узел был всегда в кэше). Код на C# есть в MSDN:

    1 public override void Execute(Guid targetInstanceId)

    2 {

    3      foreach (SPSite siteCollection in this.WebApplication.Sites)

    4      {

    5           WarmUpSiteCollection(siteCollection);

    6 

    7           siteCollection.RootWeb.Dispose();

    8           siteCollection.Dispose();

    9      }

   10 }

   11 

   12 private void WarmUpSiteCollection(SPSite siteCollection)

   13 {

   14       WebRequest request = WebRequest.Create(siteCollection.Url);

   15       request.Credentials = CredentialCache.DefaultCredentials;

   16       request.Method = "GET";

   17 

   18       WebResponse response = request.GetResponse();

   19       response.Close();

   20 }

Вот код на PowerShell, который нам необходимо вставить как параметр скрипта в наше задание:

    1 function WarmUpSiteCollection ($siteCollection)

    2 {

    3         $request = [System.Net.WebRequest]::Create($siteCollection.Url)

    4         $request.Credentials = [System.Net.CredentialCache].DefaultCredentials

    5         $request.Method = "GET"

    6 

    7         $response = $request.GetResponse()

    8         $response.Close()

    9 }

   10 

   11 foreach ($siteCollection in $this.WebApplication.Sites)

   12 {

   13        WarmUpSiteCollection $siteCollection

   14        $siteCollection.RootWeb.Dispose()

   15        $siteCollection.Dispose()

   16 }

Как видите, отличий не много.

Теперь у нас готов класс, который мы можем использовать в любое время добавлений заданий таймера в Sharepoint. Что можно с этим сделать – зависит от вашей фантазии. У меня, например, реализовано оповещение о приближающихся сроках по задачам и о просроченных задачах, плюс разные мелочи.

Собственно, выкладываю библиотеку класса. Её надо перетащить в c:\Windows\assembly в проводнике или зарегестрировать с помощью gacutil, после чего перезапустить службу Sharepoint Timer (SPTimerV3):

http://cid-9e1589588902dbaa.skydrive.live.com/embedicon.aspx/%d0%9e%d0%b1%d1%89%d0%b5%d0%b4%d0%be%d1%81%d1%82%d1%83%d0%bf%d0%bd%d1%8b%d0%b5/SPPowerShellJob.zip

 

В следующей части я расскажу и событиях списков и элементов списков.

Отправка оповещений о задачах в Sharepoint

Задачи в Sharepoint выведены в отдельный “класс” элементов. Они не просто имеют преднастроенные поля и “служат” рабочим процессам. Они также имеют преднастроенные шаблоны сообщений электронной почты, которые кроме собственно оповещения, имеют возможность также и редактировать задачу прямо из почтового клиента (конечно, если последний поддерживает такую возможность).

Например вот так это выглядит в outlook 2007:

image

image

Данная особенонсть очень удобна, т.к. пользователю не нужно открывать браузер для выполнения каких-то рутинных операций. При этом подгружаются как стандартные формы редактирования задачи, так и созданные в InfoPath (в том числе со встроенным кодом).

Это всё прекрасно до тех пора, пока нам не захочется кастомизировать текст письма. Сделать это мягко скажем не просто, особенно если нужна зависимость текста от текущего сайта и т.д. Кроме этого, по умолчанию оповещения отправляются на создание, изменение и закрытие задачи. Часто необходимы только какие-то определенные события. Поэтому многие отключают автоматические оповещения, и создают собственную систему оповещений. Реализаций несколько (на базе Workflow или обработка событий элемента списка), но все они имеют один недостаток – теряется функционал редактирования задач из почтового клиента.

Т.к. наши пользователи привыкли редактировать задачи из Outlook, пришлось разбираться, как же всё это работает.

Оказалось, что всё достаточно просто – редактирование задач построено на протоколе [MS-OSALER]: Alerts Interop Protocol Specification.

Собственно далее представлена функция на C#, реализующая этот протокол для отправки оповещений для задач в Sharepoint (не пугайтесь количеству строк кода – там много комментариев):

    1 /// <summary>

    2 /// Функция отправляет сообщения о задачи согласно протоколу MS-OSALER

    3 /// </summary>

    4 /// <param name="web">текущий узел</param>

    5 /// <param name="HtmlBody">текст письма</param>

    6 /// <param name="To">Кому</param>

    7 /// <param name="TaskItem">Элемент списка задач</param>

    8 /// <returns>[true] в случае успеха, или [false,"Error_Message"] в случае ошибки</returns>

    9 public object[] SendMail(SPWeb web, string HtmlBody , string To, SPListItem TaskItem)

   10 {

   11     try

   12     {

   13         //Данная секция формируется согласно справки на System.Net.MailAddress

   14         //

   15         //Получаем настройки сайта. Из них нам понадобяться настройки исходящей почты

   16         SPWebApplication webApp = web.Site.WebApplication;

   17         //Формируем почтовое сообщение

   18         MailMessage mess = new MailMessage();

   19         //Берем из настроек адрес ОТ

   20         mess.From = new MailAddress(webApp.OutboundMailSenderAddress, web.Title);

   21         //и адрес для ответов

   22         mess.ReplyTo = new MailAddress(webApp.OutboundMailReplyToAddress);

   23         //тело у нах в формате UTF8

   24         mess.BodyEncoding = Encoding.UTF8;

   25         //и в формате HTML

   26         mess.IsBodyHtml = true;

   27         //Записываем тело письма, оно может абсолютно любым. Хоть пустым

   28         mess.Body = HtmlBody;

   29         //собственно кому мы отправляем письмо

   30         mess.To.Add(To);

   31         //Кодировка темы письма

   32         mess.SubjectEncoding = Encoding.UTF8;

   33         //И сама тема, может быть любой.

   34         mess.Subject = "Задачи — " + TaskItem.Title;

   35 

   36         //Далее иждут обязательные параметы, которые служт для формирования заголовков согласно MS-OSALER

   37         //Attachment — это реализация MIME, так что используем пустое вложение для формирования заголовков MIME

   38         Attachment at = new Attachment(new System.IO.MemoryStream(0), "");

   39         at.ContentType = new System.Net.Mime.ContentType("text/html; charset=utf-8");

   40         at.TransferEncoding = System.Net.Mime.TransferEncoding.QuotedPrintable;

   41         //Получаем домен для формирования MessageId. Вы можете поменять это значение.

   42         string domain = webApp.OutboundMailSenderAddress.Remove(0, webApp.OutboundMailSenderAddress.LastIndexOf(‘@’));

   43         //Формируем MessageID. Оно состоит из обязательной части и случайного ИД сообщения

   44         //(последнее делает почтовый сервер, если MessageID явно не указан в заголовках)

   45         mess.Headers.Add("Message-Id", "<3BD50098E401463AA228377848493927"+Guid.NewGuid().ToString("D")+domain+">");

   46 

   47         //Этот параметр тоже можно изменить, в протоколе он указан как рекомендваный (SHOULD), но где он используется я не нашел

   48         //поэтому сделал также, как в Sharepoint по умолчанию — описание задачи

   49         mess.Headers.Add("X-Sharing-Title", this.ToBase64(TaskItem["Body"].ToString()));

   50         //А вот далее идут параметры, менять которые нельзя.

   51         mess.Headers.Add("X-AlertTitle", this.ToBase64("System"));

   52         mess.Headers.Add("X-AlertId", "{93A2F525-F664-4B02-9AD6-07851B1381C4}:{791979F1-2AB1-427D-9722-41B08012172B}");

   53         mess.Headers.Add("Content-Class", "MSWorkflowTask");

   54 

   55         mess.Headers.Add("X-AlertWebUrl", this.ToBase64(web.Url));

   56         mess.Headers.Add("X-AlertServerType", "STS");

   57 

   58         mess.Headers.Add("X-AlertWebSoap", this.ToBase64(web.Url + "/_vti_bin/alerts.asmx"));

   59         mess.Headers.Add("X-Sharing-Config-Url", "stssync://sts/?ver=1.1&type=tasks&cmd=add-folder&base-url=" + Uri.EscapeDataString(web.Url) + "&list-url=" + Uri.EscapeDataString("Lists/Tasks") + "&guid=" + Uri.EscapeDataString(TaskItem.ParentList.ID.ToString("D")));

   60         mess.Headers.Add("X-Sharing-Remote-Uid", TaskItem.ParentList.ID.ToString("D"));

   61         mess.Headers.Add("X-Sharing-WssBaseUrl", this.ToBase64(web.Url));

   62         mess.Headers.Add("X-Sharing-ItemId", this.ToBase64(TaskItem.ID.ToString()));

   63 

   64         //Заголовок сформирован, можно отправлять.

   65         //Берем адрес почтового сервера из настроек

   66         SmtpClient client = new SmtpClient(webApp.OutboundMailServiceInstance.Server.Address);

   67         client.Credentials = CredentialCache.DefaultNetworkCredentials;

   68         client.Send(mess);

   69         //Это тестовая функция, так что обработка крайне примитивная

   70         return new object[]{ true};

   71     }

   72     catch (Exception er)

   73     { return new object[]{false,er.Message }; }

   74 }

   75 

   76 

   77 string ToBase64(string InputString)

   78 {

   79     //Грубо. Может есть и более красивая реализация. Но эта точно работает.

   80     //также не плохо было бы сделать обработку исключений

   81     return "=?utf-8?B?" + System.Convert.ToBase64String(UTF8Encoding.UTF8.GetBytes(InputString))+ "?=";

   82 }

Думаю комментарий самодостаточен. Данную функцию можно использовать (после доработки конечно) для создания собственной системы оповещений.