Just a quick question, how do I get the local Bluetooth devices that have already been paired using the Motorola's EMDK 2.4 for an MC75 device? Seems I can get the RemoteDevice list but there is no method to see the local stack and what's been paired already, this way I can read what serial port it has already been assigned and open up a SerialPort object automatically for the user.
The answer is you don't or can't...you instead use the Microsoft Bluetooth. Download this for windows mobile bluetooth on a motorola device...may work on other devices too. You can get from the mobile samples, I found it here on my hard drive...C:\Program Files (x86)\Windows Mobile 6 SDK\Samples\Common\CS\Bluetooth. I added this to my project then all I had to do was this to add all the currently paired devices to a listBox
BluetoothRadio radio = new BluetoothRadio();
listBox1.DataSource = radio.PairedDevices;
listBox1.DisplayMember = "Name";
And then when one was selected you can access it as a BlueTooth device like this:
BluetoothDevice device = listBox1.SelectedItem as BluetoothDevice;
You then start stream by
if (device != null) {
BTConnectionManager.Instance.startThread(
StandardServices.SerialPortServiceGuid,
new ThreadStart(StreamProcessor));
if (BTConnectionManager.Instance.Connect(device)) {
...Do something...
I had to modifiy the StreamProcessor and BTConnectionManager a bit to work for me but here is my version of it without the form references in it.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using Microsoft.WindowsMobile.SharedSource.Bluetooth;
namespace Project2 {
/// <summary>
/// Connection Manager to marshal the data connection and reference the
/// data streams to push/pull data.
/// </summary>
public class BTConnectionManager {
BTConnectionManager() { }
/// <summary>
/// reference the Singleton and make a singleton object out of
/// this since we only want one for now.
/// </summary>
public static BTConnectionManager Instance {
get {
return Nested.instance;
}
}
/// <summary>
/// easiest way to make this a singleton that is thread safe.
/// </summary>
class Nested {
static Nested() { }
internal static readonly BTConnectionManager instance = new BTConnectionManager();
}
/// <summary>
/// The Bluetooth radio.
/// </summary>
private BluetoothRadio radio = new BluetoothRadio();
/// <summary>
/// Guid of the Bluetooth service
/// </summary>
private Guid guid;
/// <summary>
/// Thread function that processes data from the stream.
/// </summary>
private ThreadStart streamProcessor;
/// <summary>
/// The two-way communication stream to the other Bluetooth device.
/// </summary>
private NetworkStream stream;
/// <summary>
/// A BinaryReader on top of this.stream
/// </summary>
private BinaryReader reader;
/// <summary>
/// A BinaryWriter on top of this.stream
/// </summary>
private BinaryWriter writer;
/// <summary>
/// Should we stop the service thread, in preparation for
/// exiting the app?
/// </summary>
private bool exiting = false;
/// <summary>
/// The Bluetooth service.
/// </summary>
private BluetoothService bluetoothService;
/// <summary>
/// A BinaryWriter used to write to the other Bluetooth device.
/// </summary>
public BinaryWriter Writer {
get { return writer; }
}
/// <summary>
/// A BinaryReader used to read from the other Bluetooth device.
/// </summary>
public BinaryReader Reader {
get { return reader; }
}
/// <summary>
/// Gets a value indicating whether a connection is established with
/// the other Bluetooth device.
/// </summary>
public bool Connected {
get { return stream != null; }
}
/// <summary>
/// The two-way communication stream to the other Bluetooth device.
/// </summary>
private NetworkStream Stream {
get { return stream; }
set {
stream = value;
if (stream == null) {
if (writer != null) {
writer.Close();
writer = null;
}
if (reader != null) {
reader.Close();
reader = null;
}
} else {
writer = new BinaryWriter(stream);
reader = new BinaryReader(stream);
}
}
}
/// <summary>
/// Creates a new instance of a ConnectionManager.
/// </summary>
/// <param name="guid">The Bluetooth service guid.</param>
/// <param name="streamProcessor">A callback function that will read and process data from the stream.</param>
public void startThread(Guid guid, ThreadStart dataProcessor) {
this.guid = guid;
this.streamProcessor = dataProcessor;
Thread t = new Thread(new ThreadStart(ServiceThread));
t.Start();
}
/// <summary>
/// The thread that listens for Bluetooth connections, and processes
/// the data read from a connection once established.
/// </summary>
private void ServiceThread() {
bluetoothService = new BluetoothService(this.guid);
while (!exiting) {
if (!bluetoothService.Started) {
bluetoothService.Start();
}
try {
this.Stream = bluetoothService.AcceptConnection();
} catch (System.Net.Sockets.SocketException) {
// bluetoothService.Stop() was called.
// Treat this like a graceful return from AcceptConnection().
}
if (!exiting) {
// Call the streamProcessor to handle the data from the stream.
streamProcessor();
}
}
exiting = false;
}
/// <summary>
/// Force the service thread to exit.
/// </summary>
public void Exit() {
// This will cause us to fall out of the ServiceThread() loop.
exiting = true;
if (!Connected) {
// We must be waiting on AcceptConnection(), so we need to
// force an exception to break out.
bluetoothService.Stop();
}
}
/// <summary>
/// Connect to another Bluetooth device.
/// </summary>
public bool Connect(BluetoothDevice device) {
if (device != null) {
try {
this.Stream = device.Connect(this.guid);
} catch (System.Net.Sockets.SocketException) {
// Couldn't connect.
}
if (this.Stream == null) {
System.Windows.Forms.MessageBox.Show("Could not connect to device " + device.Name);
return false;
} else {
// Forcibly break out of the AcceptConnection in
// ServiceThread(), and continue on to streamProcessor().
bluetoothService.Stop();
return true;
}
}
return false;
}
/// <summary>
/// Disconnect from the other Bluetooth device.
/// </summary>
public void Disconnect() {
Stream = null;
}
internal void Disconnect(BluetoothDevice device) {
Disconnect();
}
}
}
Related
I have a Cross-Platform Xamarin app iOS and wanted to add the nfc-scan functionality.
I followed the link- https://learn.microsoft.com/en-us/xamarin/ios/platform/introduction-to-ios11/corenfc
These have been configured properly-
Info.plist privacy key.
Entitlements.plist entry.
provisioning profile with NFC Tag Reading capability.
I've put all into a new RFIDReader class which is present in a iOS Class Library, and this method gets called from my ViewModel class.
If I run this code, everything looks fine. The scan starts when I press the button on my phone, it shows the blue tick symbol but then it never gets into one of the implemented methods DidDetect, DidInvalidate.
Do you know what the reason could be?
Here's my code-
/// <summary>
/// Class for RFID form control
/// </summary>
public sealed class RFIDReader : UIViewController, IRFIDReader, INFCNdefReaderSessionDelegate
{
/// <summary>
/// The scanning session
/// </summary>
private NFCNdefReaderSession session;
/// <summary>
/// The task completion source
/// </summary>
private TaskCompletionSource<string> tcs;
/// <summary>
/// Gets the handle
/// </summary>
/// <value>The handle</value>
public new IntPtr Handle { get; }
/// <summary>
/// Read the string characters from scanning
/// </summary>
/// <returns>Task status</returns>
public Task<string> ScanCode()
{
if (!NFCNdefReaderSession.ReadingAvailable)
{
//// NFC is not available or disabled
throw new Exception(FormsErrorConstants.NearFieldCommunicationDisabled);
}
this.tcs = new TaskCompletionSource<string>();
this.session = new NFCNdefReaderSession(this, DispatchQueue.CurrentQueue, true);
this.session.BeginSession();
return this.tcs.Task;
}
/// <summary>
/// Called when a tag is successfully read
/// </summary>
/// <param name="session">Session</param>
/// <param name="messages">Messages</param>
public void DidDetect(NFCNdefReaderSession session, NFCNdefMessage[] messages)
{
var bytes = messages[0].Records[0].Payload.Skip(3).ToArray();
var message = Encoding.UTF8.GetString(bytes);
this.tcs.SetResult(message);
}
/// <summary>
/// Called when an error occurs or the 60 second timeout is reached
/// </summary>
/// <param name="session">Session</param>
/// <param name="error">Error</param>
public void DidInvalidate(NFCNdefReaderSession session, NSError error)
{
var readerError = (NFCReaderError)(long)error.Code;
if (readerError != NFCReaderError.ReaderSessionInvalidationErrorFirstNDEFTagRead &&
readerError != NFCReaderError.ReaderSessionInvalidationErrorUserCanceled)
{
// some error handling
}
this.tcs.SetResult(string.Empty);
}
/// <summary>
/// Clean up method
/// </summary>
public new void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Clean up method
/// </summary>
/// <param name="isDisposing">is disposing</param>
private new void Dispose(bool isDisposing)
{
if (isDisposing)
{
// Dispose
this.session.InvalidateSession();
this.session = null;
}
}
}
We are using Thales nShield HSM for storing Private keys and the corresponding public key is stored in the Certificate store.
We have written the logic as below:
Search for a valid slot and open a session for that on the first
call and it can serve multiple requests.
It expires after half an
hours. During the time it is not expired, if it gets any request to
serve.
Now even when it is not expired, when we try to check the
sessionInfo it is giving the below message:
Method C_GetSessionInfo returned CKR_CRYPTOKI_NOT_INITIALIZED
Please help. Thanks in Advance.
The below is the addition to the above query.
We created a class, which encapsulated all of the Pkcs11Interop usage and exposed few methods as shown below.
/// <summary>
/// Contains the information about Private key stored in HMS and Certificate to load from File System/Windows Certificates Store/HSM.
/// </summary>
public class HardwareSecureModule
{
/// <summary>
/// CryptoApi reference
/// </summary>
public string CryptoApiPath { get; set; }
/// <summary>
/// Idenfitier of the Private Key
/// </summary>
public string KeyLabel { get; set; }
/// <summary>
/// Idenfitier type of the Private Key
/// </summary>
public string KeyIdentifier { get; set; }
/// <summary>
/// Idenfitier of the Token
/// </summary>
public string TokenLabel { get; set; }
/// <summary>
/// Token Pin
/// </summary>
public string TokenPin { get; set; }
/// <summary>
/// Idenfitier of the Certificate
/// </summary>
public string CertificateLabel { get; set; }
}
public interface IHsmSession : IDisposable
{
/// <summary>
/// Find key encryption algorithm
/// </summary>
/// <returns></returns>
string GetEncryptionAlgorithm();
/// <summary>
/// sign the digest
/// </summary>
/// <param name="digest"></param>
/// <returns></returns>
byte[] Sign(byte[] digest, string encryptionAlgorithm, string hashAlgorithm);
/// <summary>
/// Indicates if thread within the pool is working
/// to avoid disposal of the same
/// </summary>
bool Locked { get; set; }
/// <summary>
/// Unique identifier of the HSM Session
/// </summary>
Guid Id { get; }
}
/// <summary>
/// Class for communicating with HSM
/// </summary>
public class Pkcs11HsmSession : IHsmSession
{
private Pkcs11 _pkcs11;
private Slot _slot;
private Session _session;
private readonly HardwareSecureModule _certificateInformation = null;
public bool Locked { get; set; }
public Guid Id { get; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="certificateInformation"></param>
public Pkcs11HsmSession(HardwareSecureModule certificateInformation)
{
Id = Guid.NewGuid();
_certificateInformation = certificateInformation;
if (_certificateInformation != null)
InitializeVariables();
}
private void InitializeVariables()
{
_pkcs11 = GetPkcs11Instance(_certificateInformation.CryptoApiPath);
if (_pkcs11 == null)
throw new Exception("Unable to create instance of Pkcs11");
_slot = FindSlot(_pkcs11, _certificateInformation.TokenLabel);
if (_slot == null)
throw new Exception("Specified token not found: " + _certificateInformation.TokenLabel);
_session = _slot.OpenSession(true);
if (_session == null)
throw new Exception("Unable to create session for the slot");
SessionLogin();
}
private Pkcs11 GetPkcs11Instance(string hsmCryptoApi)
{
Pkcs11 pkcs11 = null;
try
{
pkcs11 = CreatePkcs11Instance(hsmCryptoApi, true);
}
catch (Pkcs11Exception ex)
{
if (ex.RV == CKR.CKR_CANT_LOCK)
pkcs11 = CreatePkcs11Instance(hsmCryptoApi, false);
else
throw ex;
}
return pkcs11;
}
private Pkcs11 CreatePkcs11Instance(string hsmCryptoApi, bool useOsLocking)
{
return new Pkcs11(hsmCryptoApi, useOsLocking);
}
private Slot FindSlot(Pkcs11 pkcs11, string tokenLabel)
{
if (string.IsNullOrEmpty(tokenLabel))
throw new Exception("Token label is not specified");
List<Slot> slots = pkcs11.GetSlotList(true);
if (slots != null && slots.Count > 0)
{
foreach (Slot slot in slots)
{
TokenInfo tokenInfo = null;
try
{
tokenInfo = slot.GetTokenInfo();
}
catch (Pkcs11Exception ex)
{
if (ex.RV != CKR.CKR_TOKEN_NOT_RECOGNIZED && ex.RV != CKR.CKR_TOKEN_NOT_PRESENT)
throw;
}
if (tokenInfo == null)
continue;
if (!string.IsNullOrEmpty(tokenLabel))
if (0 !=
String.Compare(tokenLabel, tokenInfo.Label, StringComparison.InvariantCultureIgnoreCase))
continue;
return slot;
}
}
return null;
}
/// <summary>
/// HSM Signs the digest using private key
/// </summary>
/// <param name="message"></param>
/// <param name="encryptionAlgorithm"></param>
/// <param name="hashAlgorithm"></param>
/// <returns></returns>
public virtual byte[] Sign(byte[] message, string encryptionAlgorithm, string hashAlgorithm)
{
hashAlgorithm = hashAlgorithm.Replace("-", string.Empty);
CKM signingMechanismType = GetSigningMechanismType(encryptionAlgorithm, hashAlgorithm);
SessionLogin();
ObjectHandle privateKeyHandle = GetPrivateKeyHandle();
if (signingMechanismType == CKM.CKM_ECDSA)
{
message = GetMessageDigest(message, hashAlgorithm);
}
using (Mechanism mechanism = new Mechanism(signingMechanismType))
{
byte[] signedHash = _session.Sign(mechanism, privateKeyHandle, message);
if (signingMechanismType == CKM.CKM_ECDSA)
{
return ConstructEcdsaSigValue(signedHash);
}
return signedHash;
}
}
private byte[] GetMessageDigest(byte[] message, string hashAlgorithm)
{
CKM hashMechanismType = (CKM)Enum.Parse(typeof(CKM), "CKM_" + hashAlgorithm.ToUpper());
using (Mechanism mechanism = new Mechanism(hashMechanismType))
{
return _session.Digest(mechanism, message);
}
}
/// <summary>
/// Construct ECDSA der sequence
/// </summary>
/// <param name="rs"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
public byte[] ConstructEcdsaSigValue(byte[] rs)
{
if (rs == null)
throw new ArgumentNullException("rs is null");
if (rs.Length < 2 || rs.Length % 2 != 0)
throw new ArgumentException("Invalid length of rs byte");
int halfLen = rs.Length / 2;
byte[] half1 = new byte[halfLen];
Array.Copy(rs, 0, half1, 0, halfLen);
var r = new BigInteger(1, half1);
byte[] half2 = new byte[halfLen];
Array.Copy(rs, halfLen, half2, 0, halfLen);
var s = new BigInteger(1, half2);
var derSequence = new Org.BouncyCastle.Asn1.DerSequence(
new Org.BouncyCastle.Asn1.DerInteger(r),
new Org.BouncyCastle.Asn1.DerInteger(s));
return derSequence.GetDerEncoded();
}
/// <summary>
/// GetEncryptionAlgorithm for Interface
/// </summary>
/// <returns></returns>
public string GetEncryptionAlgorithm()
{
SessionLogin();
string objectAttributeValue = GetObjectAttribute().ToString();
switch ((CKK)Enum.Parse(typeof(CKK), objectAttributeValue))
{
case CKK.CKK_RSA:
return "RSA";
case CKK.CKK_ECDSA: //CKK.CKK_EC has same value as CKK.CKK_ECDSA:
return "ECDSA";
default:
throw new Exception("Unknown Encryption Algorithm");
}
}
/// <summary>
/// Get atrributes for object handle
/// </summary>
/// <returns></returns>
private ulong GetObjectAttribute()
{
ObjectHandle objectHandle = GetPrivateKeyHandle();
List<CKA> keyAttributes = new List<CKA>();
keyAttributes.Add(CKA.CKA_KEY_TYPE);
List<ObjectAttribute> keyObjectAttributes = _session.GetAttributeValue(objectHandle, keyAttributes);
return keyObjectAttributes[0].GetValueAsUlong();
}
/// <summary>
/// Extract private key handle from HSM
/// </summary>
/// <returns></returns>
private ObjectHandle GetPrivateKeyHandle()
{
_logger.WriteTrace("Inside GetPrivateKeyHandle()", LogCategory.General);
string keyLabel = _certificateInformation.KeyLabel;
string keyIdentifier = _certificateInformation.KeyIdentifier;
List<ObjectAttribute> searchTemplate = new List<ObjectAttribute>();
searchTemplate.Add(new ObjectAttribute(CKA.CKA_CLASS, CKO.CKO_PRIVATE_KEY));
CKA indentifierType;
bool parseResult = Enum.TryParse(keyIdentifier, out indentifierType);
if (!parseResult)
throw new Exception("Invalid Key Identifier '" + keyIdentifier + "'. Please provide a valid value (CKA_ID, CKA_LABEL etc).");
searchTemplate.Add(new ObjectAttribute(indentifierType, keyLabel));
List<ObjectHandle> foundObjects = _session.FindAllObjects(searchTemplate);
if (foundObjects.Count < 1)
{
throw new Exception(string.Format("Private key with {0} '{1}' was not found", keyIdentifier, keyLabel));
}
else if (foundObjects.Count > 1)
{
throw new Exception(string.Format("More than one private key with {0} '{1}' was found", keyIdentifier, keyLabel));
}
return foundObjects[0];
}
/// <summary>
/// Get MechanismType CKM for Ecdsa
/// </summary>
/// <param name="hashAlgorithm"></param>
/// <returns></returns>
private CKM GetEcdsaMechanismType(string hashAlgorithm)
{
switch (hashAlgorithm)
{
//Currently we don't have direct support for the below mechanism in HSM, however if supported this code can be uncommented and used
//case "SHA1":
// return CKM.CKM_ECDSA_SHA1;
//case "SHA224":
// return CKM.CKM_ECDSA_SHA224;
//case "SHA256":
// return CKM.CKM_ECDSA_SHA256;
//case "SHA384":
// return CKM.CKM_ECDSA_SHA384;
//case "SHA512":
// return CKM.CKM_ECDSA_SHA512;
default:
return CKM.CKM_ECDSA;
}
}
/// <summary>
/// Get CKM based upon hash algorithm
/// </summary>
/// <param name="hashAlgorithm"></param>
/// <returns></returns>
private CKM GetRsaMechanismType(string hashAlgorithm)
{
switch (hashAlgorithm)
{
case "SHA512":
return CKM.CKM_SHA512_RSA_PKCS;
case "SHA256":
default:
return CKM.CKM_SHA256_RSA_PKCS;
}
}
/// <summary>
/// Get CKM based on encryption and hash algorithm
/// </summary>
/// <param name="encryptionAlgorithm"></param>
/// <param name="hashAlgorithm"></param>
/// <returns></returns>
private CKM GetSigningMechanismType(string encryptionAlgorithm, string hashAlgorithm)
{
switch (encryptionAlgorithm)
{
case "EC":
case "ECDSA":
return GetEcdsaMechanismType(hashAlgorithm);
case "RSA":
default:
return GetRsaMechanismType(hashAlgorithm);
}
}
private void CloseSession()
{
if (_session != null)
{
try
{
SessionLogout();
}
catch
{
// Any exceptions can be safely ignored here
}
_session.Dispose();
_session = null;
}
_slot = null;
if (_pkcs11 != null)
{
_pkcs11.Dispose();
_pkcs11 = null;
}
}
public void Dispose()
{
CloseSession();
}
private void SessionLogout()
{
if (_session != null && GetSessionState() == CKS.CKS_RO_USER_FUNCTIONS)
{
ulong sessionId = _session.SessionId;
_session.Logout();
}
}
private void SessionLogin()
{
if (_session != null && GetSessionState() != CKS.CKS_RO_USER_FUNCTIONS)
{
_session.Login(CKU.CKU_USER, _certificateInformation.TokenPin);
}
}
private CKS GetSessionState()
{
try
{
return _session.GetSessionInfo().State;
}
catch (Exception ex)
{
if (_certificateInformation != null)
InitializeVariables();
return _session.GetSessionInfo().State;
}
}
}
PKCS#11 defines an application as a single process with single address space and one or multiple threads of control running in it.
Any application becomes a "Cryptoki application" by initializing PKCS#11 library in one of its threads with a call to C_Initialize function. After the library has been initialized, the application can call other functions of PKCS#11 API. When the application is done using PKCS#11 API, it finalizes PKCS#11 library with a call to C_Finalize function and ceases to be a "Cryptoki application". From application perspective, PKCS#11 library initialization and finalization are global events, so it is crucial to ensure that one thread does not finalize library while other threads are still working with it.
PKCS#11 function C_Initialize is called in constructor of HighLevelAPI.Pkcs11 class and C_Finalize function is called when instance of HighLevelAPI.Pkcs11 class is disposed. It is crucial to ensure that two instances of this class working with same PKCS#11 library do not overlap each other. My guess is that you are using more than one instance and you dispose it while you are still trying to use the other.
I implement simple ScaleoutMessageBus. I override Send method and send message to all configured nodes. And It works, but when system is under heavy load memory of w3wp process grows. When I create dump I saw that almost all memory is held in SignalR Message objects. Does any body know what I doing wrong?
Thanks
Martin
Update added ScaleOutBus class
public class ScaleoutMessageBus : Microsoft.AspNet.SignalR.Messaging.ScaleoutMessageBus
{
private static long sNextId = long.MinValue;
private readonly List<string> proxiesEndpointName;
private static readonly object Lock = new object();
/// <summary>
/// Initializes a new instance of the <see cref="ScaleoutMessageBus" /> class.
/// </summary>
/// <param name="resolver">The resolver.</param>
/// <param name="config">The configuration.</param>
public ScaleoutMessageBus(IDependencyResolver resolver, ScaleoutConfiguration config)
: base(resolver, config)
{
proxiesEndpointName = new List<string>(config.NodeEndpoints);
Open(0);
}
private void Received(int streamIndex, long id, byte[] data)
{
ScaleoutMessage message = ScaleoutMessage.FromBytes(data);
var uid = unchecked((ulong)id);
lock (Lock)
{
OnReceived(streamIndex, uid, message);
}
}
/// <summary>
/// Sends the specified stream index.
/// </summary>
/// <param name="streamIndex">Index of the stream.</param>
/// <param name="messages">The messages.</param>
/// <returns></returns>
protected override Task Send(int streamIndex, IList<Message> messages)
{
var messageToSend = new ScaleoutMessage(messages);
var bytesToSend = messageToSend.ToBytes();
return Task.Factory.StartNew(() =>
{
Parallel.ForEach(proxiesEndpointName.Where(s => !String.IsNullOrEmpty(s)), s =>
{
try
{
var proxy = new ChannelProxyWrapper<IBackplaneService>(s).GetTransparentProxy();
proxy.Send(streamIndex, bytesToSend);
}
catch (Exception ex)
{
LogHelper.WriteException(ex, TraceEventType.Warning, "Cannot send message to node {0}", s);
}
});
});
}
/// <summary>
/// Sends the specified stream index.
/// </summary>
/// <param name="streamIndex">Index of the stream.</param>
/// <param name="data">The data.</param>
public void Send(int streamIndex, byte[] data)
{
long id = Interlocked.Increment(ref sNextId);
Received(streamIndex, id, data);
}
}
i have a timer job which i want to run only once per day, for the entire farm. How do I
Deploy it in a multiple WFE environment? Do I run the stsadm -o deploysolution command in every WFE, or just the one where I want to run it?
Where should i activate the feature? Should it be activated only from a particular WFE?
What should be the value of the SPJobLockType.
It looks like you need a farm-scoped feature, that installs a service that runs this job. Here is how I did it (using code written by a colleague, to be honest, but he's not in SO).
Create a feature.xml file with a feature event receiver.
<Feature
Id="..."
Title="..."
Description="..."
Scope="Farm"
Version="1.0.0.0"
Hidden="FALSE"
ReceiverAssembly="XXX.FarmService, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxx"
ReceiverClass="XXX.FarmService.XXXFarmFeatureEventReceiver"
xmlns="http://schemas.microsoft.com/sharepoint/">
</Feature>
Inside the event receiver:
public override void OnFeatureActivated(SPFeatureReceiverProperties properties)
{
try
{
SPFarm farm = SPFarm.Local;
// Get Service, if it already exists
JobService myService = null; // JobService is a subclass of SPService
foreach (SPService service in farm.Services)
{
if (String.Compare(service.Name, JobService.XXXServiceName, true) == 0)
{
myService = (service as JobService);
break;
}
}
if (cegService == null)
{
// Create service
myService = new JobService(farm);
myService.Update();
// Create service instances
JobServiceInstance myServiceInstance; // JobServiceInstance is a subclas of SPServiceInstance
foreach (SPServer server in farm.Servers)
{
myServiceInstance = new JobServiceInstance(server, myService);
myServiceInstance.Update();
}
}
// Dayly schedule
SPDailySchedule schedule = new SPDailySchedule();
schedule.BeginHour = 1;
schedule.EndHour = 1;
schedule.BeginMinute = 0;
schedule.EndMinute = 59;
// Our own job; JobCheckDocDates is a subclass of SPJobDefinition
JobCheckDocDates newJob = new JobCheckDocDates(cegService, null, SPJobLockType.Job);
newJob.Schedule = schedule;
newJob.Update();
myService.JobDefinitions.Add(newJob);
myService.Update();
}
catch (Exception e)
{
Logger.Error("[" + properties.Feature.Definition.DisplayName + "] Error during feature activation", e);
}
}
This creates a service available on every server in the farm.
Some more details on the subclasses:
public class JobCheckDocDates: Common.BaseJob
{
/// <summary>
/// The job name
/// </summary>
public static string JobName = "XXX job";
/// <summary>
/// Constructor
/// </summary>
public JobCheckDocDates() : base() { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="service"></param>
/// <param name="server"></param>
/// <param name="lockType"></param>
public JobCheckDocDates(SPService service, SPServer server, SPJobLockType lockType)
: base(JobName, service, server, lockType) { }
...
and of course the Execute method.
public class JobService : SPService
{
public static string XXXServiceName = "XXX Service";
public override string DisplayName
{
get
{
return XXXServiceName;
}
}
public override string TypeName
{
get
{
return "XXX Service Type";
}
}
/* An empty public constructor required for serialization. */
public JobService() { }
public JobService(SPFarm farm)
: base(XXXServiceName, farm)
{
}
}
public class JobServiceInstance : SPServiceInstance
{
/// <summary>
/// Eos Service Instance Name
/// </summary>
public static string XXXServiceInstanceName = "XXXServiceInstance";
/// <summary>
/// Manage Link
/// </summary>
private SPActionLink _manageLink;
/// <summary>
/// Provision Link
/// </summary>
private SPActionLink _provisionLink;
/// <summary>
/// Unprovision Link
/// </summary>
private SPActionLink _unprovisionLink;
/// <summary>
/// Roles
/// </summary>
private ICollection<string> _roles;
/// <summary>
/// Manage Link
/// </summary>
public override SPActionLink ManageLink
{
get
{
if (_manageLink == null)
{
_manageLink = new SPActionLink(SPActionLinkType.None);
}
return _manageLink;
}
}
/// <summary>
/// Provision Link
/// </summary>
public override SPActionLink ProvisionLink
{
get
{
if (_provisionLink == null)
{
_provisionLink = new SPActionLink(SPActionLinkType.ObjectModel);
}
return _provisionLink;
}
}
/// <summary>
/// Unprovision Link
/// </summary>
public override SPActionLink UnprovisionLink
{
get
{
if (_unprovisionLink == null)
{
_unprovisionLink = new SPActionLink(SPActionLinkType.ObjectModel);
}
return _unprovisionLink;
}
}
/// <summary>
/// Roles
/// </summary>
public override ICollection<string> Roles
{
get
{
if (_roles == null)
{
_roles = new string[1] { "Custom" };
}
return _roles;
}
}
/// <summary>
/// Empty constructor
/// </summary>
public JobServiceInstance() : base() { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="server">The server</param>
/// <param name="service">The Eos service</param>
public JobServiceInstance(SPServer server, JobService service)
: base(XXXServiceInstanceName, server, service)
{
}
Now, in Central Admin, go to Operations / Services on server. Choose the desired server and start the service.
To answer your list of questions:
1. Deploy the solution just once, independently of a WFE.
2. As the feature is farm-scoped, it should be activated in Central Admin.
3. The SPJobLockType is SPJobLockType.Job
This is not exactly what you imagined, but it has the advantage of letting you easily choose where the job is run, even long after you install the feature (e.g. if a server becomes overloaded with other stuff).
The OnFeatureActivated method could be smarter and check for each server if the service exists and add it if needed. But it is simpler to remove the service from every service in OnFeatureDeactivated. So, if you add new servers, deactivate then reactivate the feature.
So far, in my research I have seen that it is unwise to set AllowUnsafeUpdates on GET request operation to avoid cross site scripting. But, if it is required to allow this, what is the proper way to handle the situation to mitigate any exposure?
Here is my best first guess on a reliable pattern if you absolutely need to allow web or site updates on a GET request.
Best Practice?
protected override void OnLoad(System.EventArgs e)
{
if(Request.HttpMethod == "POST")
{
SPUtility.ValidateFormDigest();
// will automatically set AllowSafeUpdates to true
}
// If not a POST then AllowUnsafeUpdates should be used only
// at the point of update and reset immediately after finished
// NOTE: Is this true? How is cross-site scripting used on GET
// and what mitigates the vulnerability?
}
// Point of item update
using(SPSite site = new SPSite(SPContext.Current.Site.Url, SPContext.Current.Site.SystemAccount.UserToken))
{
using (SPWeb web = site.RootWeb)
{
bool allowUpdates = web.AllowUnsafeUpdates; //store original value
web.AllowUnsafeUpdates = true;
//... Do something and call Update() ...
web.AllowUnsafeUpdates = allowUpdates; //restore original value
}
}
Feedback on the best pattern is appreciated.
If you're performing any operations which modify something, then anyone that can convince the user to click on a link can perform that operation. For instance, let's assume that you have a GET request to a page which lets the user add an administrator to a site, and the user clicks a link to a page which does a Response.Redirect("http://yourserver/_layouts/admin.aspx?operation=addAdministrator&username=attackerNameHere").
While normally a POST does not offer much protection against this (nothing will stop someone from having a <form method="post" action="http://yourserver/_layouts/admin.aspx">), SharePoint has a concept of form digests, which contain information about the previous request that is generating the post back (including the user's name). This reduces the footprint for this kind of attack significantly.
The only time that it is not a security issue to AllowUnsafeUpdates on a GET is if you're not taking input from the user. For instance, if you have a web part which also logs visits to a list, then there's no security vulnerability exposed.
Edit: If you are going to use AllowUnsafeUpdates, there's no need to reset it to its previous value. It does not get persisted. It's just something you need to set on an SPWeb object before performing updates from a GET (or other cases)
I would slightly modify Trent's delegate to accept the web to update:
public static void DoUnsafeUpdate(this SPWeb web, Action<SPWeb> action)
{
try
{
web.AllowUnsafeUpdates = true;
action(web);
}
finally
{
web.AllowUnsafeUpdates = false;
}
}
And then extend HttpContext to encapsulate verification of the form digest, with an option to elevate using the technique described here:
public static void DoUnsafeUpdate(this HttpContext context, Action<SPWeb> action, bool elevated)
{
SPWeb web = SPControl.GetContextWeb(context);
if (!context.Request.HttpMethod.Equals("POST", StringComparison.Ordinal)
|| web.ValidateFormDigest())
throw new SPException("Error validating postback digest");
if (elevated)
web.RunAsSystem(w => w.DoUnsafeUpdate(action));
else
web.DoUnsafeUpdate(action);
}
Usage:
protected override void OnLoad(System.EventArgs e)
{
Context.DoUnsafeUpdate(web =>
{
// Update elevated web
}, true);
}
Another clean way to implement would be to use a combination of extension methods and anonymous delegates as such:
public static void DoUnsafeUpdate(this SPWeb web, Action action)
{
bool allowUnsafeUpdates = web.AllowUnsafeUpdates;
web.AllowUnsafeUpdates = true;
action();
web.AllowUnsafeUpdates = allowUnsafeUpdates;
}
Using the above extension method, you can then perform your "unsafe update" action as follows:
var web = SPContext.Current.Web;
web.DoUnsafeUpdate(delegate()
{
// Put your "unsafe update" code here
});
For AllowUnsafeUpdates, I follow this process:
if( HttpContext.Current is null )
{
Do nothing, no need to set AllowUnsafeUpdates to true nor
to call ValidateFormDigest() because update will be carried out
}
else // HttpContext.Current is NOT null
{
if( SPContext.Current is null )
{
Need to set AllowUnsafeUpdates to true
}
else // SPContext.Current is NOT null
{
Call ValidateFormDigest()
}
}
Not so sure it is worth remembering the previous value of allow unsafe updates.
I would want to wrap the call around the minimum possible amount of code, so that nested calls to it would not occur.
Then you can just turn it to false afterwards.
I use a wrapper class for handling most manipulation of SPWeb objects. This helps me remember to close the web, and it eases the problems of unsafeupdates setting. It is a bit bloated, as I have patched on new constructors and members. but, then again; so is the SPWeb class.
Usage:
using (WebWrapper wrapper = new WebWrapper("http://localhost"))
{
wrapper.AllowUnsafeUpdates();
//Do work on wrapper.
}
The class definition:
using System;
using System.Collections.Specialized;
using System.Data;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.Serialization;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
namespace Skaar.SharePoint.Customization
{
/// <summary>
/// A wrapper for a <see cref="SPWeb"/> object.
/// <remarks>Closes web object on Dispose if applicable.</remarks>
/// </summary>
[Serializable]
[DebuggerDisplay("{Uri} Unsafe:{AllowUnsafeUpdatesSetting} Update:{UpdatePending}")]
public sealed class WebWrapper : IDisposable, IDeserializationCallback, IEquatable<WebWrapper>
{
[NonSerialized] private bool unsafeUpdatesSetting;
[NonSerialized] private SPWeb web;
/// <summary>
/// Determines if the inner web object should be closed.
/// </summary>
[NonSerialized] private bool webShouldBeClosed;
/// <summary>
/// This value is used in serialization to restore <see cref="Web"/>.
/// </summary>
private string webUrl;
/// <summary>
/// Creates a new wrapper object.
/// </summary>
/// <param name="web">A web that should be closed/disposed when done.</param>
public WebWrapper(SPWeb web) : this(web, true)
{
}
/// <summary>
/// Creates a new wrapper object.
/// </summary>
/// <param name="web">An inner web object</param>
/// <param name="webShouldBeClosed">If true, the web object is closed in the <see cref="Dispose()"/> method.</param>
public WebWrapper(SPWeb web, bool webShouldBeClosed)
{
setWeb(web, webShouldBeClosed);
}
/// <summary>
/// Creates a new wrapper object.
/// </summary>
/// <param name="webAddress">The address to a web.</param>
public WebWrapper(Uri webAddress)
{
using (SPSite site = new SPSite(webAddress.ToString()))
{
string relativeUrl = renderWebRootRelativeUrl(webAddress);
if (relativeUrl == null)
{
setWeb(site.OpenWeb(), true);
}
else
{
setWeb(site.OpenWeb(relativeUrl), true);
}
}
}
private string renderWebRootRelativeUrl(Uri address)
{
for (int i = 0; i < address.Segments.Length; i++)
{
string segment = address.Segments[i];
if (string.Equals(segment, "_layouts/"))
{
string newUrl=string.Join(null, address.Segments, 0, i).Trim('/');
return newUrl;
}
}
return null;
}
/// <summary>
/// If true, <see cref="SPWeb.Update"/> will be called in <see cref="Dispose()"/>.
/// </summary>
public bool UpdatePending { get; private set; }
/// <summary>
/// The setting of the inner web (<see cref="SPWeb.AllowUnsafeUpdates"/>)
/// </summary>
public bool AllowUnsafeUpdatesSetting
{
get { return Web.AllowUnsafeUpdates; }
}
/// <summary>
/// The inner object.
/// </summary>
/// <exception cref="ObjectDisposedException">Exception is thrown if <see cref="IsDisposed"/> is true.</exception>
public SPWeb Web
{
get
{
if(IsDisposed)
{
throw new ObjectDisposedException("Web wrapper is disposed.");
}
return web;
}
}
/// <summary>
/// The address of the <see cref="Web"/> wrapped as a <see cref="Uri"/> object.
/// </summary>
public Uri Uri
{
get { return new Uri(Web.Url); }
}
/// <summary>
/// The address of the <see cref="Web"/> wrapped as a <see cref="Uri"/> object.
/// </summary>
public Uri GetUri(SPUrlZone zone)
{
return Site.WebApplication.GetResponseUri(zone, Uri.AbsolutePath);
}
/// <summary>
/// Creates a wrapper around the context web.
/// <remarks>The web will not be closed when wrapper is disposed. Returns null if context is unavailable.</remarks>
/// </summary>
public static WebWrapper Context
{
get
{
return SPContext.Current==null?null:new WebWrapper(SPContext.Current.Web, false);
}
}
/// <summary>
/// This is a static property wrapping of
/// the <see cref="CloneOf(SPWeb)"/> method, using
/// the <see cref="SPContext"/> current web as
/// parameter.
/// <remarks>Returns null if context is unavailable.</remarks>
/// </summary>
public static WebWrapper CloneOfContext
{
get
{
if (SPContext.Current != null)
{
SPWeb contextWeb = SPContext.Current.Web;
return CloneOf(contextWeb);
}
return null;
}
}
/// <summary>
/// Returns the <see cref="SPWeb.Exists"/> property of the <see cref="Web"/> object.
/// </summary>
public bool Exists
{
get { return Web != null && Web.Exists; }
}
/// <summary>
/// Gets the <see cref="SPSite"/> object of <see cref="Web"/>.
/// </summary>
/// <remarks>This object should not be closed by user code.</remarks>
public SPSite Site
{
get { return web.Site; }
}
/// <summary>
/// Gets the owner defined in <see cref="SPSite.Owner"/>.
/// </summary>
public SPUser Owner
{
get
{
return Site.Owner;
}
}
/// <summary>
/// Returns a context of the inner <see cref="Web"/>.
/// </summary>
public SPContext ContextOfWeb
{
get { return SPContext.GetContext(web); }
}
/// <summary>
/// Gets the language of <see cref="Web"/>.
/// </summary>
public CultureInfo Locale
{
get { return Web.Locale; }
}
/// <summary>
/// Gets the language of the root web.
/// </summary>
public CultureInfo LocaleOfRoot
{
get
{
using (WebWrapper root = Root)
{
return root.Locale;
}
}
}
/// <summary>
/// Returns a new <see cref="WebWrapper"/> wrapping the root <see cref="SPWeb"/> of this.
/// </summary>
public WebWrapper Root
{
get
{
if (webShouldBeClosed)
using (SPSite site = Site)
{
return new WebWrapper(site.RootWeb);
}
return new WebWrapper(Site.RootWeb);
}
}
/// <summary>
/// A wrapper for <see cref="SPWeb.Title"/>.
/// </summary>
public string Title
{
get { return Web.Title; }
set { Web.Title = value; }
}
/// <summary>
/// A wrapper for <see cref="SPWeb.ID"/>.
/// </summary>
public Guid ID
{
get { return Web.ID; }
}
#region Web Properties
[NonSerialized] private bool updatePropertiesPending;
/// <summary>
/// A wrapper method to <see cref="Web"/> object's <see cref="SPWeb.Properties"/> indexer.
/// </summary>
/// <param name="key">The key to use when fetching property value.</param>
/// <returns>A string containing the value.</returns>
public string GetProperty(string key)
{
return Web.Properties[key];
}
/// <summary>
/// Sets the value in the <see cref="Web"/> object's <see cref="SPWeb.Properties"/>. Creates a new key, or updates an existing as needed.
/// </summary>
/// <param name="key">The key to use when storing the property value.</param>
/// <param name="value">The value to set in the key.</param>
/// <remarks>The property <see cref="UpdatePending"/> is set to true.</remarks>
public void SetProperty(string key, string value)
{
if (!Web.Properties.ContainsKey(key))
{
Web.Properties.Add(key, value);
}
else
{
Web.Properties[key] = value;
}
updatePropertiesPending = true;
}
#endregion
#region IDeserializationCallback Members
///<summary>
///Runs when the entire object graph has been deserialized.
///</summary>
///
///<param name="sender">The object that initiated the callback. The functionality for this parameter is not currently implemented. </param>
public void OnDeserialization(object sender)
{
using (SPSite site = new SPSite(webUrl))
{
setWeb(site.OpenWeb(), true);
}
}
#endregion
#region IDisposable Members
///<summary>
///Closes inner web object if appropriate.
///</summary>
///<filterpriority>2</filterpriority>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Dispose(bool isDisposing)
{
if (IsDisposed) return;
if (isDisposing)
{
doDisposeOfWeb();
IsDisposed = true;
}
}
#endregion
/// <summary>
/// Value is true if <see cref="Dispose()"/> method has been called. Object is not in a usable state.
/// </summary>
internal bool IsDisposed
{
get; private set;
}
#region IEquatable<WebWrapper> Members
/// <summary>
/// This tests whether the two objects wraps the same web. It may however be two different instances of the same web.
/// </summary>
/// <param name="other">Another wrapper object.</param>
/// <returns>True if <see cref="Uri"/> equals, false otherwise.</returns>
public bool Equals(WebWrapper other)
{
if (other == null)
{
return false;
}
return Uri.Equals(other.Uri);
}
#endregion
/// <summary>
/// Reopens the inner <see cref="SPWeb"/> object. May be used when web object needs to be rereferenced in a new security context.
/// </summary>
public void ReOpen()
{
bool unsafeSetting = AllowUnsafeUpdatesSetting;
using (SPSite site = new SPSite(Web.Url))
{
SPWeb newWeb = site.OpenWeb();
doDisposeOfWeb();
web = newWeb;
web.AllowUnsafeUpdates = unsafeSetting;
unsafeUpdatesSetting = false;
webShouldBeClosed = true;
}
}
private void doDisposeOfWeb()
{
if (Web == null) return;
Update(true);
if (webShouldBeClosed)
{
Web.Close();
}
else if (Web.Exists)
{
Web.AllowUnsafeUpdates = unsafeUpdatesSetting;
}
web = null;
}
/// <summary>
/// Calls <see cref="SPWeb.Update"/> on the <see cref="Web"/> object.
/// </summary>
public void Update()
{
Update(false);
}
/// <summary>
/// Sets <see cref="UpdatePending"/> to <c>true</c>.
/// </summary>
public void SetUpdatePending()
{
UpdatePending = true;
}
/// <summary>
/// Calls <see cref="SPWeb.Update"/> on the <see cref="Web"/> object.
/// <param name="onlyIfPending">If true, update will depend on state of the <see cref="UpdatePending"/> property.</param>
/// </summary>
public void Update(bool onlyIfPending)
{
if (onlyIfPending)
{
if (updatePropertiesPending)
{
Web.Properties.Update();
updatePropertiesPending = false;
}
if (UpdatePending)
{
Web.Update();
UpdatePending = false;
}
}
else
{
Web.Update();
UpdatePending = false;
}
}
/// <summary>
/// Returns the list from <see cref="Web"/> with <see cref="SPList.Title"/> equal to <see cref="title"/>.
/// </summary>
/// <param name="title">The <see cref="SPList.Title"/> of an existing list.</param>
/// <returns>The first list found with the given title, or null, if no list is found.</returns>
public SPList GetList(string title)
{
foreach (SPList list in Web.Lists)
{
if (list.Title == title)
{
return list;
}
}
return null;
}
/// <summary>
/// A wrapper method to the <see cref="Web"/> object's <see cref="SPWeb.Lists"/> indexer.
/// </summary>
/// <param name="id">The id of the list to return.</param>
/// <returns>The list with the supplied id.</returns>
public SPList GetList(Guid id)
{
return Web.Lists[id];
}
private void setWeb(SPWeb innerWeb, bool shouldBeClosed)
{
if (innerWeb == null || !innerWeb.Exists)
{
throw new ArgumentException("Web does not exist", "innerWeb");
}
web = innerWeb;
webShouldBeClosed = shouldBeClosed;
unsafeUpdatesSetting = innerWeb.AllowUnsafeUpdates;
AllowUnsafeUpdates();
webUrl = web.Url;
}
/// <summary>
/// Creates a new <see cref="SPWeb"/> object using the
/// url of the <see cref="web"/> parameter and wraps it
/// in a new wrapper object. The web will be
/// closed when the wrapper is disposed.
/// The cloning is done using the <see cref="SPWeb.Url"/>, thus no security context is transferred to the new web.
/// </summary>
/// <remarks>Use this to create a clone of the context web.</remarks>
/// <param name="web">The web to clone.</param>
/// <returns>A new wrapper object.</returns>
public static WebWrapper CloneOf(SPWeb web)
{
using (SPSite site = new SPSite(web.Url))
{
return new WebWrapper(site.OpenWeb());
}
}
/// <summary>
/// Creates a new <see cref="SPWeb"/> object using the
/// <see cref="Web"/> of the <see cref="web"/> parameter and wraps it
/// in a new wrapper object. The web will be
/// closed when the wrapper is disposed.
/// </summary>
/// <remarks>Use this to create a clone of the context web.</remarks>
/// <param name="web">The wrapper to clone.</param>
/// <returns>A new wrapper object.</returns>
public static WebWrapper CloneOf(WebWrapper web)
{
return CloneOf(web.Web);
}
/// <summary>
/// Sets the AllowUnsafeUpdates property to true on the
/// wrapped web object.
/// <remarks>
/// The setting is resat back in the dispose method, unless the
/// web itself is closed.
/// </remarks>
/// </summary>
public void AllowUnsafeUpdates()
{
Web.AllowUnsafeUpdates = true;
}
/// <summary>
/// Returns the url of the inner web.
/// </summary>
/// <returns>A value that equals <see cref="Web"/> <see cref="SPWeb.Url"/> property.</returns>
public override string ToString()
{
return webUrl;
}
/// <summary>
/// Returns a new <see cref="WebWrapper"/> object wrapping a new copy of the inner <see cref="Web"/> object.
/// The cloning is done using the <see cref="SPWeb.Url"/>, thus no security context is transferred to the new web.
/// </summary>
/// <remarks>The static method <see cref="CloneOf(SPWeb)"/> is used on the <see cref="Web"/> property.</remarks>
/// <returns>A new wrapper.</returns>
public WebWrapper Clone()
{
return CloneOf(Web);
}
/// <summary>
/// Implicitly wraps the web object in a <see cref="WebWrapper"/> object.
/// </summary>
/// <param name="web">The web to wrap.</param>
/// <returns>A new wrapper object. The original web may be accessed through the <see cref="Web"/> property.</returns>
public static implicit operator WebWrapper(SPWeb web)
{
return new WebWrapper(web, false);
}
/// <summary>
/// Explicitly extracts the <see cref="Web"/> value from the <see cref="wrapper"/>.
/// </summary>
/// <param name="wrapper">The object wrapping the <see cref="SPWeb"/> to extract.</param>
/// <returns>The inner <see cref="Web"/> of <see cref="wrapper"/>.</returns>
/// <remarks>The returned <see cref="SPWeb"/> object should be properly disposed after use.</remarks>
public static explicit operator SPWeb(WebWrapper wrapper)
{
wrapper.DoNotDisposeInnerWeb();
return wrapper.Web;
}
/// <summary>
/// Wrapper method for <see cref="SPWeb.GetList"/> on <see cref="Web"/> object.
/// </summary>
/// <param name="uri">A site relative uri to the list.</param>
/// <returns>A list if found.</returns>
public SPList GetList(Uri uri)
{
return web.GetList(uri.ToString());
}
/// <summary>
/// Wrapper method for <see cref="SPWeb.GetSiteData"/> on <see cref="Web"/> object.
/// </summary>
/// <returns>The results of the query,</returns>
public DataTable GetSiteData(SPSiteDataQuery query)
{
return Web.GetSiteData(query);
}
/// <summary>
/// Creates a new <see cref="SPWeb"/> as a sub web to this.
/// </summary>
/// <param name="url">The proposed local url of the new web. The nearest available is selected.</param>
/// <param name="name">The title of the new web.</param>
/// <param name="description">The description of the new web.</param>
/// <param name="language">The language of the new web. <remarks>If the language is not supported, the language of this is used.</remarks></param>
/// <param name="template">The site template to use.</param>
/// <returns>The new web wrapped in a new <see cref="WebWrapper"/> object.</returns>
[DebuggerStepThrough]
//debugger step through is to prevent this method to break when debugging, as it throws exceptions by [poor] design.
public WebWrapper CreateSubWeb(string url, string name, string description, uint language,
string template)
{
SPWeb newWeb;
try
{
newWeb = Web.Webs.Add(findSuitableWebUrl(url), name, description, language, template, true, false);
}
catch (SPException err)
{
if (err.ErrorCode == -2130575266)
{
//language not supported. Fallback to parent web language
newWeb = Web.Webs.Add(findSuitableWebUrl(url), name, description, Web.Language, template, true,
false);
}
else
throw;
}
return new WebWrapper(newWeb);
}
private string findSuitableWebUrl(string proposedName)
{
StringCollection names = new StringCollection();
names.AddRange(Web.Webs.Names);
int suffixIndex = 0;
const int maxIterations = 100000;
string name = proposedName;
while (names.Contains(name) && suffixIndex < maxIterations)
{
name = string.Format("{0}_{1}", proposedName, suffixIndex++);
}
return name;
}
/// <summary>
/// Calling this method will inhibit the default behaviour of closing the web on disposal.
/// </summary>
/// <remarks>Use with caution.</remarks>
internal void DoNotDisposeInnerWeb()
{
webShouldBeClosed = false;
}
}
}