Sunday, December 20, 2015

Remove X-Frame-Options = SAMEORIGIN HTTP header in Sharepoint or allow Sharepoint site to be shown in iframe

By default Sharepoint 2013 adds X-Frame-Options = SAMEORIGIN HTTP header to the response for better security (in order to avoid clickjacking attacks). However because of that Sharepoint site may be shown in iframe only inside the same site, i.e. it is not possible to show it in iframe inside another external site. Sometime this requirement becomes more important and we will need to allow sites to be shown in iframes. In this post I will show how to solve this issue.

We will use URL Rewrite IIS module for changing value of X-Frame-Options HTTP header from SAMEORIGIN to empty string. Although it is not 100% correct behavior because allowed values for the header are: DENY, SAMEORIGIN, ALLOW-FROM (last one doesn’t work in FF and Chrome at the moment of writing this article), i.e. if we don’t need this header we need to remove it completely. However with URL Rewrite it is only possible to change headers, not remove it (I tried to remove it by adding <remove name=”X-Frame-Options” /> to system.webServer > httpProtocol > customHeaders in web.config, but it didn’t help). And with empty value IE, FF and Chrome allow site to be opened in iframe.

First of all we need to install URL Rewrite IIS extension if it is not done yet. After that go to IIS Manager, select appropriate Sharepoint site and click URL Rewrite on the right side. Create new empty outbound rule like it is shown on the following picture:

Note that you need to specify variable name as RESPONSE_X-Frame-Options, not just X-Frame-Options. And you should not add neither RESPONSE_X-Frame-Options nor X-Frame-Options to URL Rewrite > Allowed Server Variables, like it is shown in some articles.

If you will check web.config rule should look like this:

   1: <rewrite>
   2:   <outboundRules>
   3:     <rule name="Rule1" patternSyntax="Wildcard" stopProcessing="false">
   4:       <match serverVariable="RESPONSE_X-Frame-Options" pattern="*" />
   5:       <action type="Rewrite" value="" />
   6:     </rule>
   7:   </outboundRules>
   8: </rewrite>

Now if you will check response from your Sharepoint site in Fiddler you will see that X-Frame-Options header is empty:

After that it will be possible to show your site in iframe. Hope it will help someone, but anyway don’t forget about security.

Monday, December 14, 2015

Send email to external users in Sharepoint 2013 workflow

As you probably know Sharepoint 2013 supports 2 workflow platforms:

  • Workflows 2010
  • Workflows 2013

Using of workflows 2013 in Sharepoint 2013 requires installation and configuration of Workflow manager – there are many guides available in internet at the moment so I won’t add it there. It gives us many new useful features like loops. Unfortunately there are also some limitation comparing with 2010 workflows. For example it is not so simple now to sent emails to external users. In 2013 workflow when you add action Send email you should specify valid (resolvable) Sharepoint user with non-empty email, not email itself. It was done for security purposes. Because of this you can’t anymore send email to external users as simple as it was in workflows 2010 where you could just specify user’s email in To field.

In order to avoid this issue we used Plumsail’s Workflow Actions Pack. Please note that it is commercial product, prices are available on their web site. It also has trial 30-days version when you may try this product. Documentation available on Plumsail doesn’t provide all necessary information which is needed for making actions pack work, so I will describe several additional steps.

When you will install the package there will be number of new actions available in Sharepoint Designer 2013. One of them is “Send email with attachments (SMTP)” (there is also possibility to send email via Exchange, but I didn’t try it). In this action together with email settings (recipient, subject, body) you need to specify SMTP host, port, ssl usage. Note that you may only use those SMTP servers which work with authenticated users. And you need to specify credentials in Email and Password fields of the workflow action together with other SMTP parameters (initially Plumsail documentation mentioned that these parameters belong to Exchange user, but at the moment of writing of this post it was already fixed – good work from their support. Also in Email field it is not necessary to specify real email. Some SMTP servers authenticate users with separate user id, which is not email). If you will leave these fields empty you will get the following exception;

Global Exception LoggerException: System.NullReferenceException: Object reference not set to an instance of an object.    
at Plumsail.WFServices.Common.SmtpEmailRepository.I1e(String  )    
at Plumsail.WFServices.Common.SmtpEmailRepository.SendEmail(Email email)    
at Plumsail.WFServices.Services.ExchangeController.SendEmail(EmailSendRequest request)    
at SyncInvokeSendEmail(Object , Object[] , Object[] )    
at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)    
at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)    
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)    
at System.ServiceModel..Dispatcher.ImmutableDispatchRuntime.ProcessMessage31(MessageRpc& rpc)    
at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)

This is one of limitations, because you can’t use your internal company SMTP server which is available only internally and doesn’t require authentication.

If you specified all SMTP settings with Email and Password you may also get the following error:

Exception: Could not load file or assembly 'Microsoft.Exchange.WebServices, Version=15.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. The system cannot find the file specified.

In order to fix it you need to install MS Exchange web services API, which is available here. It may also confuse a bit, because Exchange API is needed in scenarios when you use SMTP. However after installation, it should successfully send emails to external users.

Tuesday, December 8, 2015

Speaking with lecture “Using .Net expression trees for creating translators on C#” in Computer Science Center in St-Petersburg

Yesterday I presented open lecture “Using .Net expression trees for creating translators on C#” in Computer Science Center in St-Petersburg: http://open.compscicenter.ru/. Computer Science Center – is initiative of leading IT companies in St-Petersburg which is intended to prepare students to work in IT industry by providing advanced education in computer science (programming languages, algorithms, etc.). It was interesting format and interesting experiences, thanks to all participants. Presentation from the lecture is available on SlideShare: http://www.slideshare.net/sadomovalex/net-c-55910243.

Tuesday, December 1, 2015

Copy taxonomy field values using javascript object model in Sharepoint

Suppose that we have file in source list and we need to copy this file with metadata to target list. Source and target lists may be located on different site collections. In this post I will show how to copy metadata form source file to target using javascript object model. This approach may be useful when you need to perform similar task in Sharepoint Online.

Let’s assume that we need to copy 3 taxonomy fields: Region, Country and Language. It can be done using the following code:

   1: var ctx = ...
   2: var sourceItems = ...
   3: var targetList = ...
   4: var targetItem = ...
   5:  
   6: var enumerator = sourceItems.getEnumerator();
   7: enumerator.moveNext();
   8: var sourceItem = enumerator.get_current();
   9:  
  10: var fieldRegion = targetList.get_fields().getByInternalNameOrTitle("Region");
  11: var fieldRegionTax = ctx.castTo(fieldRegion, SP.Taxonomy.TaxonomyField);
  12: ctx.load(fieldRegionTax);
  13:  
  14: var fieldCountry = targetList.get_fields().getByInternalNameOrTitle("Country");
  15: var fieldCountryTax = ctx.castTo(fieldCountry, SP.Taxonomy.TaxonomyField);
  16: ctx.load(fieldCountryTax);
  17:  
  18: var fieldLanguage = targetList.get_fields().getByInternalNameOrTitle("Language");
  19: var fieldLanguageTax = ctx.castTo(fieldLanguage, SP.Taxonomy.TaxonomyField);
  20: ctx.load(fieldLanguageTax);
  21:  
  22: ctx.executeQueryAsync(
  23:     Function.createDelegate(this, function (sender, args) {
  24:         getTaxonomyFieldsSuccess(ctx, sourceItem, targetItem, fieldRegionTax,
  25:     fieldCountryTax, fieldLanguageTax); }),
  26:     Function.createDelegate(this, function (sender, args) {
  27:         console.log("Load taxonomy fields failed: " + args.get_message() + "\n" +
  28:             args.get_stackTrace()); }));
  29:  
  30: function getTaxonomyFieldsSuccess(ctx, sourceItem, targetItem,
  31:     fieldRegionTax, fieldCountryTax, fieldLanguageTax) {
  32:     if (sourceItem.get_item("Region") != null) {
  33:         var targetValue = new SP.Taxonomy.TaxonomyFieldValue();
  34:         targetValue.set_label(sourceItem.get_item("Region").get_label());
  35:         targetValue.set_termGuid(sourceItem.get_item("Region").get_termGuid());
  36:         targetValue.set_wssId(sourceItem.get_item("Region").get_wssId());
  37:         fieldRegionTax.setFieldValueByValue(targetItem, targetValue);
  38:     }
  39:  
  40:     if (sourceItem.get_item("Country") != null) {
  41:         var targetValue = new SP.Taxonomy.TaxonomyFieldValue();
  42:         targetValue.set_label(sourceItem.get_item("Country").get_label());
  43:         targetValue.set_termGuid(sourceItem.get_item("Country").get_termGuid());
  44:         targetValue.set_wssId(sourceItem.get_item("Country").get_wssId());
  45:         fieldCountryTax.setFieldValueByValue(targetItem, targetValue);
  46:     }
  47:  
  48:     if (sourceItem.get_item("Language") != null) {
  49:         var targetValue = new SP.Taxonomy.TaxonomyFieldValue();
  50:         targetValue.set_label(sourceItem.get_item("Language").get_label());
  51:         targetValue.set_termGuid(sourceItem.get_item("Language").get_termGuid());
  52:         targetValue.set_wssId(sourceItem.get_item("Language").get_wssId());
  53:         fieldLanguageTax.setFieldValueByValue(targetItem, targetValue);
  54:     }
  55:  
  56:     targetItem.update();
  57:     ctx.executeQueryAsync(
  58:         Function.createDelegate(this, function (sender, args) {
  59:             console.log("Metadata is copied successfully");
  60:         }),
  61:         Function.createDelegate(this, function (sender, args) {
  62:             console.log("Copying metadata failed: " + args.get_message() + "\n" +
  63:                 args.get_stackTrace());
  64:         }));
  65: }

At first we load taxonomy fields from the target list (lines 10-21). Then having these fields set their value using label, term guid and wssId properties of the source field value (lines 32-54).

Friday, November 27, 2015

Create custom ribbon button in list view for Sharepoint Online using client object model

In this post I will show how to create custom ribbon button in specific list for Sharepoint Online. Suppose that we have the following xml file with declaration of the ribbon button:

   1: <UserCustomAction Location="CommandUI.Ribbon"
   2:     Sequence="5" Title="Get tasks">
   3: <![CDATA[<CommandUIExtension>
   4:   <CommandUIDefinitions>
   5:     <CommandUIDefinition Location="Ribbon.Documents.Manage.Controls._children">
   6:       <Button Id="Ribbon.ListItem.Manage.GetTasks"
   7:         Alt="Get tasks"
   8:         Sequence="10001"
   9:         Command="Get tasks_Button"
  10:         Image32by32="/_layouts/images/rtrsendtoicon.png"
  11:         Image16by16="/_layouts/images/copy16.gif"
  12:         LabelText="Get tasks"
  13:         TemplateAlias="o1" />
  14:     </CommandUIDefinition>
  15:   </CommandUIDefinitions>
  16:   <CommandUIHandlers>
  17:    <CommandUIHandler Command="Get tasks_Button" CommandAction="javascript: GetTasks();" />
  18:   </CommandUIHandlers>
  19: </CommandUIExtension>]]>
  20: </UserCustomAction>

For more convenient work we create own class for that in order to be able to deserialize it from xml:

   1: public class UserCustomActionEntity
   2: {
   3:     [XmlAttribute("Location")]
   4:     public string Location;
   5:     [XmlAttribute("Sequence")]
   6:     public int Sequence;
   7:     [XmlAttribute("Title")]
   8:     public string Title;
   9:     [XmlText]
  10:     public string CommandUIExtension;
  11: }

With these preparations ribbon button can be created using the following code:

   1: var web = ...;
   2: var list = ...;
   3: var existingActions = list.UserCustomActions;
   4: web.Context.Load(existingActions);
   5: web.Context.ExecuteQueryRetry();
   6:  
   7: bool exists = false;
   8: foreach (var a in existingActions)
   9: {
  10:     if (string.Compare(a.Title, ac.Title, false) == 0)
  11:     {
  12:         exists = true;
  13:         break;
  14:     }
  15: }
  16:  
  17: if (exists)
  18: {
  19:     return;
  20: }
  21:  
  22: var action = existingActions.Add();
  23: action.Location = ac.Location;
  24: action.Sequence = ac.Sequence;
  25: action.Title = ac.Title;
  26: action.CommandUIExtension = ac.CommandUIExtension;
  27: action.Update();
  28: web.Context.ExecuteQuery();

Here we first check that same ribbon button is not added already by comparing titles (lines 7-20) and if not, add new custom ribbon button using data from xml declaration (lines 22-28).

Monday, November 23, 2015

Problem in Sharepoint Online with TaxonomyFieldValue represented as Object

Some time ago we faced with interesting problem in Sharepoint Online: there was javascript code which worked properly when it was called from ribbon button shown in list view. This code copied documents with metadata from one document library to another using javascript object model:

   1: var targetFile = null;
   2: var files = targetFolder.get_files();
   3: var enumerator = files.getEnumerator();
   4: while (enumerator.moveNext()) {
   5:     var file = enumerator.get_current();
   6:     if (file.get_name() == fileName) {
   7:         targetFile = file;
   8:         break;
   9:     }
  10: }
  11:  
  12: if (targetFile == null) {
  13:     return;
  14: }
  15:  
  16: var targetItem = targetFile.get_listItemAllFields();
  17: if (sourceItem.get_item("DocumentStatus") != null) {
  18:     // update target item's DocumentStatus field
  19:     // ...
  20: }

However same code didn’t work when it was called from wiki page. When I checked the result of sourceItem.get_item(“DocumentStatus”) call, which supposed to return TaxonomyFieldValue instance, I found that it actually returned Object instance:

image

After that I checked output html for list view and wiki page and found that some system js files were missing in the last one. After some experimenting I found that following 3 files are needed on wiki page in order to make TaxonomyFieldValue available:

   1: <script type="text/javascript" src="/_layouts/15/sp.runtime.js"></script>
   2: <script type="text/javascript" src="/_layouts/15/sp.js"></script>
   3: <script type="text/javascript" src="/_layouts/15/sp.taxonomy.js"></script>

Added them to ScriptEditorWebPart on wiki page and after that result became returned as TaxonomyFieldValue instance:

image

And code started to work also on wiki page.