Exporting Recurring Events

What the heck, folks! Why is this so difficult!? Why is everything to do with calendars so difficult!? Actually, there are some perfectly logical reasons, but it’s more fun to complain than explain. Anyway, let’s get to what you’re all here for.

I’m going for the Guinness World Record longest blog post in this one, so for those of you who just want the code, I’ve posted it in full here (I’ve posted it as a PDF because WordPress was having difficulty rendering such a large code block).

To start, let’s talk a bit about how calendar recurrence works. Basically, all the information about the event is contained in one file. So it’s only really one event. The recurrence data is stored as information about that file, and whatever calendar tool you are using reads it and displays instances of that single event accordingly.

The main problem we run into when exporting from SharePoint to iCal, is that SharePoint stores that data (or RecurrenceData, to refer to it by its internal name) as XML, while iCal files need to create an RRULE (recurrence rule) in ics. And of course, while both of these languages communicate the exact same information, there is no one-to-one correlation we can use to easily convert between them.

To learn a little more about ics and RRULEs, visit http://www.kanzaki.com/docs/ical/. And a great resource for learning about SharePoint RecurrenceData is http://recurrence-events-in-sharepoint.blogspot.com/. And of course, there’s always Google!

While we’re at it, here’s a great blog about Sharepoint calendars: https://aspnetguru.wordpress.com/2007/06/01/understanding-the-sharepoint-calendar-and-how-to-export-it-to-ical-format/. Pay special attention to the different types of IDs and their structures, as well as the different EventTypes.

The MasterSeriesItemID is The ID of the recurring event from which this instance, exception, or deleted instance was made. It is present as the first number in the ID of the recurrence instance, formatted: #.0.{ISO Date/Time}. And it is present as the third number in the ID of the exception or deletion instance, formatted: #.1.#.

There are also four EventTypes you need to be aware of: EventType 0 is a single event. EventType 1 is a recurring/master event. EventType 3 is a deleted instance of a recurring event. And EventType 4 is an exception instance of a recurring event.

Anyway, the algorithm we’re going to use is deceptively simple:

  1. Retrieve the RecurrenceData of the master item and convert it to ics.
  2. Retrieve the ics of any exceptions to the recurrence pattern.
  3. Retrieve the ics of the master item.
  4. Insert the RecurrenceData and exceptions ics into the ics of the master item.
  5. Turn the whole shebang into a downloadable file!

Easy-peasy, here we go! The first thing I’m going to do is declare some variables:

var recStuff;
var startStuff;
var siteUrl;
var completeRule = "RRULE:"
var parser;
var xmlDoc;
var recNode1;
var endThing = "";
var weekStart;
var myXML;
var fullICS;
var masterID;
var urlpath;
var exceptionIDs = [];
var myListName;
var exceptionsICS = [];
var ListID;
var LoopCount;
var myUID;
var myRecurrenceErrorMSG = "Your browser does not support this functionality. Please try again using a different browser.";

I’m declaring these as universal variables because I’ve split the functionality into a number of different functions because I like being a sane person, and this way all of those functions have access to the same information. If you don’t particularly care about being a sane person, feel free to do all this crap as one giant function, and then you can declare these as function values. Your farm admin will be most appreciative, and will send fruit baskets to you in your padded cell. The orderlies will, of course, remove any toothpicks or skewers.

As we progress, it will become apparent what each of these is for. If you’ve already looked at the code, you’ll notice the next thing I’m doing is actually detecting the browser:

function recurDetectBrowser() {
	var ua = navigator.userAgent.toLowerCase(); 
	if (ua.indexOf('safari') != -1) { 
		if (ua.indexOf('chrome') > -1) {
			retrieveListItems();
		} 
		else {
			alert(myRecurrenceErrorMSG) // Safari
		}
	}
	else {
		retrieveListItems();
	}
}

The reason I’m doing this is that browsers like Safari don’t support the download functionality we will be employing at the end (downloading a blob file). I guess Apple thought it was a security risk? Anyway, why bother going through all the work if you can’t reap the fruits of your labor? So if the user is on Safari, they’ll get a nice little error message. And if not, the code will call the next function. If anybody knows of a good workaround for this, please let me know.

So here we go! Let’s get that RecurrenceData!

function retrieveListItems() {	
	siteUrl = "/"+window.location.pathname.split("/")[1];
	myListName = window.location.pathname.split("/")[3];
	var clientContext = new SP.ClientContext(siteUrl);
	var oList = clientContext.get_web().get_lists().getByTitle(myListName);
	var itemIDarray = window.location.toString().match(/ID=.*/i).toString().replace(/ID=|&.*/gi, "").toString().split(".");
	if(itemIDarray[1]=="0"){
		masterID = itemIDarray[0];
	}
	else {
		masterID = itemIDarray[2];
	}	
	var theXML = "<View><Query><Where><Geq><FieldRef Name=\'ID\'/><Value Type=\'Number\'>"+masterID+"</Value></Geq></Where></Query><RowLimit>1</RowLimit></View>";        
	var camlQuery = new SP.CamlQuery();
	camlQuery.set_viewXml(theXML);
	this.collListItem = oList.getItems(camlQuery);        
	clientContext.load(collListItem);        
	clientContext.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));
}

The first couple things I want to do is get the site and list names. I’m parsing through the URL to do this, as I am calling this code from a button situated on the event DispForm. So all that information is accessible to me. Next I’m parsing the URL to get the MasterSeriesItemID (using what we learned about its various formats). And all of this information is being passed into relevant variables for later use.

Next I’m setting up my CAML query to pull the information I want from the item (using the MasterSeriesItemID as the identifier), and executing that query. Now, I can hear you saying “but why not just use REST to pull that data? It’d be so much simpler!” Well, yeah. In a perfect world I would do that. And in a perfect world all the raindrops would be lemon drops and gumdrops. But (as yet) REST doesn’t support pulling RecurrenceData, and raindrops are just boring, old, life-giving water.

If the query succeeds I’m moving on to parse the specific pieces of information I want (and if it fails I’m throwing an alert box, but you can do whatever you want):

function onQuerySucceeded(sender, args) {
	var listItemInfo = '';
	var startItemInfo = '';
	var listItemEnumerator = collListItem.getEnumerator();
	while (listItemEnumerator.moveNext()) {
	var oListItem = listItemEnumerator.get_current();
	listItemInfo += 
	oListItem.get_item('RecurrenceData');
		startItemInfo +=
			oListItem.get_item('EventDate');
	}
	recStuff = listItemInfo.toString();
	var startDate1 = new Date(startItemInfo);
	startStuff = startDate1.toJSON();
	if(startStuff.length>20) {
		startStuff = startStuff.replace(/\..*Z/gi, "Z");
	}
	doXML();
	getTheDeletions();
}

function onQueryFailed(sender, args) {
	alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
}

Specifically, I’m pulling the RecurrenceData (that XML string storing the recurrence pattern), and the EventDate (the start time of that master event). Then I’m doing a little bit of data transformation. I’m setting the XML as a readable string, and the start time as a JSON date (making sure it’s in the right format). I’m saving these values to their respective variables, and calling my next two functions. The first of which begins to parse the recurrence pattern to ics, and the second of which discovers any instances deleted from the recurrence pattern.

Ok, now things are getting fun. Now we get to parse/translate that XML RecurrenceData to an ics RRULE. I found a really helpful blog post from Jonathan Huss, who had done this using C#. I highly recommend checking it out: http://blog.jonathanhuss.com/sharing-a-sharepoint-online-calendar-via-icalendar/. I pretty much just went line by line through his code and converted it from C# to JavaScript, so I couldn’t have done this without him.

So, first I’m gonna parse the XML a bit and get some basic information:

function doXML(){
	myXML = recStuff.toUpperCase();
	if (typeof DOMParser == "undefined") {
		xmlDoc = new ActiveXObject('Microsoft.XMLDOM');
		xmlDoc.async = false;
		xmlDoc.loadXML(myXML);
	}
	else {
		parser = new DOMParser();
		xmlDoc = parser.parseFromString(myXML,"text/xml");
	}
//get freq
	recNode1 = xmlDoc.getElementsByTagName("REPEAT")[0].childNodes[0];
	var rep1 = recNode1.nodeName;
//get end
	var endNode = xmlDoc.getElementsByTagName("RULE")[0].childNodes[(xmlDoc.getElementsByTagName("RULE")[0].childNodes.length)-1];
	if(xmlDoc.getElementsByTagName("REPEATFOREVER").length==1){
		//do nothing
	}
	if(xmlDoc.getElementsByTagName("REPEATINSTANCES").length==1){
		endThing = ";COUNT="+endNode.childNodes[0].nodeValue;
	}
	if(xmlDoc.getElementsByTagName("WINDOWEND").length==1){
		endThing = ";UNTIL="+endNode.childNodes[0].nodeValue.replace(/\W/g,"");
	}
//get wkst
	weekStart = ";WKST="+xmlDoc.getElementsByTagName("FIRSTDAYOFWEEK")[0].childNodes[0].nodeValue;
//switch freq
	switch(rep1) {
	case "DAILY":
		dailyFunc1();
		break;
	case "WEEKLY":
		weeklyFunc1();
		break;
	case "MONTHLY":
		monthlyFunc1();
		break;
	case "MONTHLYBYDAY":
		monthlyBYDAYFunc1();
		break;
	case "YEARLY":
		yearlyFunc1();
		break;
	case "YEARLYBYDAY":
		yearlyBYDAYFunc1();
		break;
	default:
		// default code block
	}
}

You’ll notice that I’m converting my XML string to upper case. I like to get a consistent case with this stuff to avoid any case-sensitivity issues. Normally I’d convert to lower case, but ics prefers upper, so I figured I’d take the path of least resistance. Then I’m making is JavaScript readable, and pulling the frequency, termination, and weekStart.

In XML there are something like 12 possible frequencies. Fortunately, SharePoint only uses six of them: daily, weekly, monthly, monthlybyday, yearly, and yearlybyday. There are three possibilities for termination: repeatforever (no termination), which translated to ics as null; repeatinstances (repeat a specific number of times), which translates to ics as “COUNT;” and windowend (repeat until specified date), which translates to ics as “UNTIL.” WeekStart is pretty self-explanatory (what day does the calendar week start on?), and translates pretty easily from XML to ics.

Then I’m using a switch statement to navigate to the next function (depending upon the frequency type). So let’s look at how to parse a “DAILY” frequency:

function dailyFunc1(){
//set frequency
	completeRule = completeRule+"FREQ=DAILY";
//set interval
	interval = recNode1.getAttribute("DAYFREQUENCY");
	if(interval){
		completeRule = completeRule+";INTERVAL="+interval;
	}
//set end and weekStart
	completeRule = completeRule+endThing+weekStart;
//set byday
	var ifbyday = recNode1.getAttribute("WEEKDAY");
	if(ifbyday){
		completeRule = completeRule+";BYDAY=MO,TU,WE,TH,FR";
	}
}

As you can see, I’m setting the frequency value, then the interval (does it occur every 1 days? Every 2? And etc.), and adding the termination info and weekStart. Next I’m checking to see if it occurs on weekdays only, and if so I’m adding that to an ics parameter called “BYDAY,” which simply specifies which days of the week the event occurs on. And I’m writing everything back to my RRULE variable (which I’ve called completeRule). One down, five to go!

Let’s look at “WEEKLY” next:

function weeklyFunc1(){
//set frequency
	completeRule = completeRule+"FREQ=WEEKLY";
//set interval
	interval = recNode1.getAttribute("WEEKFREQUENCY");
	if(interval){
		completeRule = completeRule+";INTERVAL="+interval;
	}
//set end and weekStart
	completeRule = completeRule+endThing+weekStart;
//byDay
	var sun = recNode1.getAttribute("SU");
	var mon = recNode1.getAttribute("MO");
	var tue = recNode1.getAttribute("TU");
	var wed = recNode1.getAttribute("WE");
	var thu = recNode1.getAttribute("TH");
	var fri = recNode1.getAttribute("FR");
	var sat = recNode1.getAttribute("SA");
	if(sun||mon||tue||wed||thu||fri||sat){
		completeRule = completeRule+";BYDAY=";
		var weekStr = "";
		if(sun){
			weekStr = weekStr+"SU,";
		}
		if(mon){
			weekStr = weekStr+"MO,";
		}
		if(tue){
			weekStr = weekStr+"TU,";
		}
		if(wed){
			weekStr = weekStr+"WE,";
		}
		if(thu){
			weekStr = weekStr+"TH,";
		}
		if(fri){
			weekStr = weekStr+"FR,";
		}
		if(sat){
			weekStr = weekStr+"SA,";
		}
		weekStr = weekStr.slice(0,-1);
		completeRule = completeRule+weekStr;
	}
}

It starts off the same as daily: frequency, interval, end/weekStart. But the BYDAY stuff gets a little more complicated. Weekly items can occur on any day/combination of days a user wants, so you have to be really careful and specific when parsing that information to ics. Overall though, still not too bad.

Even monthly is pretty easy. The only difference is that you have to specify what date of the month the event starts on and write that into the “BYMONTHDAY” parameter:

function monthlyFunc1(){
//set frequency
	completeRule = completeRule+"FREQ=MONTHLY";
//set interval
	interval = recNode1.getAttribute("MONTHFREQUENCY");
	if(interval){
		completeRule = completeRule+";INTERVAL="+interval;
	}
//set end and weekStart
	completeRule = completeRule+endThing+weekStart;
//set byday
	var whatday = recNode1.getAttribute("DAY");
	if(whatday){
		completeRule = completeRule+";BYMONTHDAY="+whatday;
	}
}

Monthlybyday steps it up a little:

function monthlyBYDAYFunc1(){
//set frequency
	completeRule = completeRule+"FREQ=MONTHLY";
//set interval
	interval = recNode1.getAttribute("MONTHFREQUENCY");
	if(interval){
		completeRule = completeRule+";INTERVAL="+interval;
	}
//set end and weekStart
	completeRule = completeRule+endThing+weekStart;
//set byday
	var weekStr = "";
	var sun = recNode1.getAttribute("SU");
	var mon = recNode1.getAttribute("MO");
	var tue = recNode1.getAttribute("TU");
	var wed = recNode1.getAttribute("WE");
	var thu = recNode1.getAttribute("TH");
	var fri = recNode1.getAttribute("FR");
	var sat = recNode1.getAttribute("SA");
	if(sun||mon||tue||wed||thu||fri||sat){
		weekStr = ";BYDAY=";
		if(sun){
			weekStr = weekStr+"SU,";
		}
		if(mon){
			weekStr = weekStr+"MO,";
		}
		if(tue){
			weekStr = weekStr+"TU,";
		}
		if(wed){
			weekStr = weekStr+"WE,";
		}
		if(thu){
			weekStr = weekStr+"TH,";
		}
		if(fri){
			weekStr = weekStr+"FR,";
		}
		if(sat){
			weekStr = weekStr+"SA,";
		}
		weekStr = weekStr.slice(0,-1);
	}
	if(recNode1.getAttribute("WEEKDAY")){
		weekStr = ";BYDAY=MO,TU,WE,TH,FR";
	}
	if(recNode1.getAttribute("WEEKEND_DAY")){
		weekStr = ";BYDAY=SA,SU";
	}
	completeRule = completeRule+weekStr;
	var whatday = recNode1.getAttribute("WEEKDAYOFMONTH")
		switch(whatday) {
		case "FIRST":
			whatday = ";BYSETPOS=1";
			break;
		case "SECOND":
			whatday = ";BYSETPOS=2";
			break;
		case "THIRD":
			whatday = ";BYSETPOS=3";
			break;
		case "FOURTH":
			whatday = ";BYSETPOS=4";
			break;
		case "LAST":
			whatday = ";BYSETPOS=-1";
			break;
		default:
			// default code block
		}
	completeRule = completeRule+whatday;
}

The BYDAY stuff is similar to a weekly item. But it can also be specified as weekday (similar to a daily item), or weekend_day (which we haven’t seen before, but works on the same principal as weekday). The final added wrinkle is weekdayofmonth. XML uses this value to determine which week of the month to put the event on. And ics records this information in its “BYSETPOS” parameter (basically, setting the position of the event).

Yearly is also pretty simple:

function yearlyFunc1(){
//set frequency
	completeRule = completeRule+"FREQ=YEARLY";
//set interval
	interval = recNode1.getAttribute("YEARFREQUENCY");
	if(interval){
		completeRule = completeRule+";INTERVAL="+interval;
	}
//set end and weekStart
	completeRule = completeRule+endThing+weekStart;
//set bymonth
	var ybymonth = recNode1.getAttribute("MONTH");
	if(ybymonth){
		completeRule = completeRule+";BYMONTH="+ybymonth;
	}
//set byday
	var ybyday = recNode1.getAttribute("DAY");
	if(ybyday){
		completeRule = completeRule+";BYMONTHDAY="+ybyday;
	}
}

The XML adds a month parameter (“BYMONTH” in ics) which records which month the event should occur in every year. And then a day parameter, which in ics is the “BYMONTHDAY” parameter we saw in the monthly pattern (which day of the specified month should it occur on?).

Ok, how’s your head? Are you beginning to feel that nagging pain? Well, take some aspirin, because that’s about to become a full-blown migraine.

YearlyByDay is about as complicated as it gets. But let’s see if we can make any sense of it:

function yearlyBYDAYFunc1(){
//set frequency
	completeRule = completeRule+"FREQ=YEARLY";
//set interval
	interval = recNode1.getAttribute("YEARFREQUENCY");
	if(interval){
		completeRule = completeRule+";INTERVAL="+interval;
	}
//set end and weekStart
	completeRule = completeRule+endThing+weekStart;
//set bymonth
	var ybymonth = recNode1.getAttribute("MONTH");
	if(ybymonth){
		completeRule = completeRule+";BYMONTH="+ybymonth;
	}
//set byday
	var weekStr = "";
	var sun = recNode1.getAttribute("SU");
	var mon = recNode1.getAttribute("MO");
	var tue = recNode1.getAttribute("TU");
	var wed = recNode1.getAttribute("WE");
	var thu = recNode1.getAttribute("TH");
	var fri = recNode1.getAttribute("FR");
	var sat = recNode1.getAttribute("SA");
	if(sun||mon||tue||wed||thu||fri||sat){
		weekStr = ";BYDAY=";
		if(sun){
			weekStr = weekStr+"SU,";
		}
		if(mon){
			weekStr = weekStr+"MO,";
		}
		if(tue){
			weekStr = weekStr+"TU,";
		}
		if(wed){
			weekStr = weekStr+"WE,";
		}
		if(thu){
			weekStr = weekStr+"TH,";
		}
		if(fri){
			weekStr = weekStr+"FR,";
		}
		if(sat){
			weekStr = weekStr+"SA,";
		}
		weekStr = weekStr.slice(0,-1);
	}
	completeRule = completeRule+weekStr;
	var daystr = "";
	if(recNode1.getAttribute("DAY")){
		var wdom = recNode1.getAttribute("WEEKDAYOFMONTH")
		switch(wdom) {
		case "FIRST":
			daystr = ";BYMONTHDAY=1";
			break;
		case "SECOND":
			daystr = ";BYMONTHDAY=2";
			break;
		case "THIRD":
			daystr = ";BYMONTHDAY=3";
			break;
		case "FOURTH":
			daystr = ";BYMONTHDAY=4";
			break;
		case "LAST":
			daystr = ";BYMONTHDAY=-1";
			break;
		default:
			// default code block
		}
	}
	var wdomstr = "";
	if(recNode1.getAttribute("WEEKDAY")||recNode1.getAttribute("WEEKEND_DAY")){
		if(recNode1.getAttribute("WEEKDAY")){
			daystr = ";BYDAY=MO,TU,WE,TH,FR";
		}
		if(recNode1.getAttribute("WEEKEND_DAY")){
			daystr = ";BYDAY=SA,SU";
		}
		var wdom = recNode1.getAttribute("WEEKDAYOFMONTH")
		switch(wdom) {
			case "FIRST":
			wdomstr = ";BYMONTHPOS=1";
			break;
		case "SECOND":
			wdomstr = ";BYMONTHPOS=2";
			break;
		case "THIRD":
			wdomstr = ";BYMONTHPOS=3";
			break;
		case "FOURTH":
			wdomstr = ";BYMONTHPOS=4";
			break;
		case "LAST":
			wdomstr = ";BYMONTHPOS=-1";
			break;
		default:
			// default code block
		}
	}	
	completeRule = completeRule+daystr+wdomstr;
}

So, frequency, interval, termination, weekStart, and BYMONTH. So far so good. Next we’ve got some BYDAY stuff. That’s pretty straight forward: what day of the week does it start on? After that, we’re parsing the weekdayofmonth stuff to a new ics parameter, BYMONTHDAY. Nothing to be afraid of there, it’s just another way of prescribing which week of the month the event belongs in.

Next we gotta deal with weekday vs. weekend_day stuff again. Super annoying, but for some reason necessary. We’re just checking for those values, and if either of them is present, setting the BYDAY stuff accordingly. The final wrinkle is, again, which weekday or weekend_day to put it on. so we’ve gotta set some position stuff (this time BYMONTHPOS rather than BYSETPOS… I don’t know why).

But that’s it! That’s the most painful part done with! How ya doing? Did you survive? You’ll notice that I’m nesting pretty much everything in if statements. This is just to be extra-precautionary. I really want to avoid any extra or empty parameters ending up in my final ics. This is a technique I will continue to employ throughout the rest of this code.

Anyway, moving on. Let’s get dem deletions!

function getTheDeletions() {
	var clientContext = new SP.ClientContext(siteUrl);
	var oList = clientContext.get_web().get_lists().getByTitle(myListName);
	var theXML = "<View><Query><Where><And><Geq><FieldRef Name=\'MasterSeriesItemID\'/><Value Type=\'Number\'>"+masterID+"</Value></Geq><Geq><FieldRef Name=\'EventType\'/><Value Type=\'Number\'>3</Value></Geq></And></Where></Query><RowLimit>100</RowLimit></View>";
	var camlQuery = new SP.CamlQuery();
	camlQuery.set_viewXml(theXML);
	this.collListItem = oList.getItems(camlQuery);
	clientContext.load(collListItem);
	clientContext.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceededForDeletions), Function.createDelegate(this, this.onQueryFailed));
}

Pretty simple (compared to what we’ve just done). We’re querying the list, this time looking for items that match our current MasterSeriesItemID but have an EventType of 3 (a deleted instance). I set the row limit to 100, figuring that would be big enough for anything I would have to deal with. But feel free to do with that what you will. Again, I’m not using REST so that I can query expanded recurrence…s.

Next I’m parsing the returned values:

function onQuerySucceededForDeletions(sender, args) {
	var delStarts = [];
	var listItemEnumerator = collListItem.getEnumerator();
	while (listItemEnumerator.moveNext()) {
	var oListItem = listItemEnumerator.get_current();
	delStarts.push(
			new Date(oListItem.get_item('EventDate')).toJSON().replace(/-|:|\./g, "")
			);
	}
	if(delStarts.length>0) {
		completeRule = completeRule+"\nEXDATE:"+delStarts.join();
		getICS();
	}
	else {
		getICS();
	}
}

I’m grabbing the EventDate (the start date and time) for each discovered instance and I’m pushing them to an array variable as JSON dates stripped of any special characters (since that’s the format ics wants… *sigh*… none of these languages even agree on a basic date format). Finally, I’m joining the array variable as a comma-separated-string and appending it to my RRULE variable (completeRule) as a parameter called EXDATE (exception dates). And I’m throwing in a newline for good measure—just when you thought you had me all figured out! You don’t know me!

Next I’m getting the ics of the master event, and plugging my RRULE and EXDATE into it near the top (right after the event’s ID, or UID in ics):

function getICS() {
	var xhttp = new XMLHttpRequest();
	xhttp.onreadystatechange = function() {
	if (xhttp.readyState == 4 && xhttp.status == 200) {
		var ics1 = xhttp.responseText;
		myUID = ics1.match(/UID;.*/gi).toString().replace(/;/gi, ":");
		var icsArray = ics1.split("UID;");
		fullICS = icsArray[0]+completeRule+"\nUID:"+icsArray[1];
		getTheExceptions();
	}
	};
	ListID = document.querySelector("[id*='RequestAccess']").getAttribute("onMenuClick").toString().replace(/.*=|/gi, "").toString().replace("');", "");
	var url1= window.location.pathname;
	urlpath=url1.replace(/\/lists.*/gi, "");
	var mid1 = "/_vti_bin/owssvr.dll?CS=109?Cmd=Display&CacheControl=1&List=";
	var mid2 = "&ID=";
	var midZero = ".0."
	var end1 = "&Using=event.ics";
	var myURL = urlpath+mid1+ListID+mid2+masterID+midZero+startStuff+end1;
	xhttp.open("GET", myURL, true);
	xhttp.send();  
}

Now, the order of operations here is kinda arbitrary. For me this just seemed the most logical place to put this step. But over all, it doesn’t really matter. The most important thing to note here is that I’m not actually pulling the ics of the master recurrence event. I’m actually pulling the ics of the very first recurrence INSTANCE. This is important because, for the master event, SharePoint records the EndDate as the recurrence termination date (so pulling that would result in one very long event, rather than a series of recurring shorter events). This is also why I put this step here rather than earlier. I wanted to pull the reccurenceData FIRST… so that I could pick up the information I needed regarding the initial occurrence… so that I could in turn pull its ics. Super fun!

I’m saving all this mess back to a master ics variable I decided to call fullICS. And I’m also saving the aforementioned UID to one of my universal variables for potential later use.

Ok, almost there! Now let’s get any exceptions to the recurrence pattern:

function getTheExceptions() {
	var clientContext = new SP.ClientContext(siteUrl);
	var oList = clientContext.get_web().get_lists().getByTitle(myListName);
	var theXML = "<View><Query><Where><And><Geq><FieldRef Name=\'MasterSeriesItemID\'/><Value Type=\'Number\'>"+masterID+"</Value></Geq><Geq><FieldRef Name=\'EventType\'/><Value Type=\'Number\'>4</Value></Geq></And></Where></Query><RowLimit>100</RowLimit></View>";
	var camlQuery = new SP.CamlQuery();
	camlQuery.set_viewXml(theXML);
	this.collListItem = oList.getItems(camlQuery);
	clientContext.load(collListItem);
	clientContext.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceededForExceptions), Function.createDelegate(this, this.onQueryFailed));
}
function onQuerySucceededForExceptions(sender, args) {
	var exStarts = [];
	var listItemEnumerator = collListItem.getEnumerator();
	while (listItemEnumerator.moveNext()) {
		var oListItem = listItemEnumerator.get_current();
		exStarts.push(
			new Date(oListItem.get_item('EventDate')).toJSON().replace(/-|:|\./g, "")
			);
		exceptionIDs.push(
			oListItem.get_item('ID')
			);
	}
	if(exceptionIDs.length>0) {
		LoopCount = exceptionIDs.length;
		for (i = 0; i < exceptionIDs.length; i++) {
			getExceptionsICS();
		}
	}
	else {
		writetofile();
	}
}

This is very similar to what we did for deletions. But you’ll notice that this time I’m pulling items with an EventType of 4 (modified instances). When running this, you might notice some overlap between the deleted and modified instances. This is because when you modify an instance in SharePoint, it actually does two things: first, it deletes the instance resulting in and EventType 3; and second, it creates a new instance resulting in an EventType 4. So ultimately both end up existing. Efficient, no!?

You might also notice that I’m using a for loop to call my ics querying/parsing function. And I can hear you typing your angry “use a forEach” comments. Yes, I can hear you typing at your computer in the future. I can hear you, OK!? And yes, I would love to do that, this would make everything that much simpler, and that much shorter. But until forEach is 100% universally supported, we’re all just gonna have to live with this cobbled-together for loop. Deal with it.

Anyway, let’s parse this farce:

function getExceptionsICS() {
	var xhttp = new XMLHttpRequest();
	xhttp.onreadystatechange = function() {
	if (xhttp.readyState == 4 && xhttp.status == 200) {
		var curStart = xhttp.responseText.match(/DTSTART:.*/gi).toString().replace(/DTSTART:/gi, "");
		var curICS = xhttp.responseText.replace(/uid;.*/gi, "RECURRENCE-ID:"+curStart+"\n"+myUID);
		curICS = curICS.replace(/BEGIN:VCALENDAR|END:VCALENDAR|PRODID:.*|VERSION:.*|METHOD:.*/gi, "");
		exceptionsICS.push(curICS);
		LoopCount--;
		if(LoopCount==0) {
			fullICS = fullICS.replace(/METHOD:.*/gi, "METHOD:PUBLISH\nX-MS-OLK-FORCEINSPECTOROPEN:TRUE");
			fullICS = fullICS.replace(/END:VCALENDAR/gi, exceptionsICS.join("")+"\nEND:VCALENDAR");
			writetofile();
		}
	}
	};
	var mid1 = "/_vti_bin/owssvr.dll?CS=109?Cmd=Display&CacheControl=1&List=";
	var mid2 = "&ID=";
	var midOne = ".1."
	var end1 = "&Using=event.ics";
	var myURL = urlpath+mid1+ListID+mid2+exceptionIDs[i]+midOne+masterID+end1;
	xhttp.open("GET", myURL, true);
	xhttp.send();
}

Mainly what I’m doing here is pulling the ics of any exceptions and pushing it to an array variable. I am doing a few strange things in the process, though. First, I’m replacing the items’ UIDs with a new parameter called RECURRENCE-ID. Second, I’m then going back on that and adding in the UID I saved from my master item. And third I’m stripping the CALENDAR header and footer off of the ics. I’m doing the first two simply because that’s how ics likes to handle IDs for recurring items. I don’t make the rules, man. And I’m doing the third because the next thing I’m doing is plugging this new array of modified ics back into my master ics (fullICS).

Yes, so once I’m finished with my lovely for loop, I’m joining the ics array with no delimiter (ics neither needs nor wants one in this case), plugging it into my master ics towards the end, and calling my penultimate function!

function writetofile() {
	if (typeof Blob !== "undefined") {
		// use the Blob constructor
		var blob = new Blob([fullICS], {type: 'text/calendar'});
		var url = URL.createObjectURL(blob);
		DownloadWindow = window.open(url, "_parent", "DownloadWindow", "location=1,status=1,scrollbars=1,  width=100,height=100");
	}
	else if (window.MSBlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder) {
		// use the supported vendor-prefixed BlobBuilder
		var bb = new BlobBuilder();
		bb.append(fullICS);
		var blob = bb.getBlob('text/calendar');
		var url = URL.createObjectURL(blob);
		DownloadWindow = window.open(url, "_parent", "DownloadWindow", "location=1,status=1,scrollbars=1,  width=100,height=100");
	}
	else {
		// neither Blob constructor nor BlobBuilder is supported
		alert(myRecurrenceErrorMSG);
	}
	CleanUp1();
}

What I’m doing here is building a text/calendar file (a JavaScript blob) based on my master ics (fullICS), giving that file a URL, and downloading it. I’m not aware of a way to do this for browsers that don’t support blobs, and again, this method of downloading doesn’t seem to fly for Safari. So hit me up if you know of a workaround for either of these things.

In the application I wrote this for, we had a requirement to record and track who had downloaded what, so I had another little function to write those names/actions to another list. But I figured that wouldn’t be as universally useful, so I haven’t included it hear. But finally, I’m just doing some cleanup:

function CleanUp1(){
	recStuff = null;
	startStuff = null;
	siteUrl = "/News";
	completeRule = "RRULE:"
	parser = null;
	xmlDoc = null;
	recNode1 = null;
	endThing = "";
	weekStart = null;
	myXML = null;
	fullICS = null;
	masterID = null;
	urlpath = null;
	exceptionIDs = [];
	myListName = null;
	exceptionsICS = [];
	ListID = null;
	LoopCount = null;
	myUID = null;
}

All this is for is to set those universal variables back to their starting values, so that if a user invokes this functionality twice in the same session, they don’t end up with a garbled/duplicated file.

And that’s all! I hope you enjoyed this post. Please do reach out to me if you have any questions, comments, suggested improvements, job offers, or general praise. All are appreciated!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s