HEX
Server: Microsoft-IIS/8.5
System: Windows NT YDAWBH120 6.3 build 9600 (Windows Server 2012 R2 Standard Edition) AMD64
User: tentjecom_web (0)
PHP: 7.4.14
Disabled: NONE
Upload Files
File: D:/HostingSpaces/PvdBoogaard/indoorski.nl/backup/oude-site/cms/modules/calendar/module.calendar.php
<?php
/**
* This file contains most of the core PHP code associated with the IWP Calendar module
*
* @package IWP_Modules
* @subpackage Calendar
*/

//	the timezone mapper can use an implementation of InterspireStash to cache it's lookups, which we also use in the core events system
InterspireICalendarTimezoneMapper::$_stash = iwp_eventstash::getInstance();

/**
 * Model for calendar exceptions.
 * @author Gwilym
 * @package IWP_Modules
 * @subpackage Calendar
 */
class iwp_module_calendar_exception extends iwp_base {
	/**
	 *
	 * @var iwp_module_calendar_exception
	 */
	public static $Instance;

	/**
	 *
	 * @var array
	 */
	protected $tableFields = array(
		'event',
		'exdate',
	);

	/**
	 *
	 * @return iwp_module_calendar_exception
	 */
	public static function getInstance () {
		if(!isset(self::$Instance)){
			self::$Instance = new self();
		}
		return self::$Instance;
	}
}

/**
 * Data model for calendar occurrences produced by repetition rules
 * @author Gwilym
 * @package IWP_Modules
 * @subpackage Calendar
 */
class iwp_module_calendar_occurrence {
	/**
	 *
	 * @var iwp_module_calendar_event
	 */
	public static $Instance;

	/**
	 *
	 * @var int
	 */
	public $id;

	/**
	 *
	 * @var iwp_module_calendar_event
	 */
	public $event;

	public $begin;

	public $end;

	/**
	 *
	 * @return iwp_module_calendar_event
	 */
	public static function getInstance () {
		if(!isset(self::$Instance)){
			self::$Instance = new self();
		}
		return self::$Instance;
	}
}

/**
 * CalendarRepeat Field Class
 * This class is used by the api form class to generate a field for editing a calendar event's repeat options.
 *
 * @package IWP_Modules
 * @subpackage Calendar
 */
class iwp_field_calendarrepeat extends iwp_field {
	/**
	 * This is the form field type of the field
	 *
	 * @var string
	 */
	public $type = 'calendarrepeat';

	/**
	 * For holding whether the label should be shown for this field or not
	 *
	 * @var Boolean
	**/
	protected $showLabel = false;

	/**
	 * __construct
	 * The constructor which calls the parent constructor that sets up the field name if it is passed in during the initialization
	 *
	 * @var string
	 */
	public function __construct($name=null){
		parent::__construct($name);
	}

	/**
	 * GetFieldOutput
	 * Returns the HTML for this field. It generates the relevant parts, assigns them to template variables and returns a parse template file.
	 *
	 * @return string Returns the field HTML
	 */
	public function GetFieldOutput($setOnly=false){
		$inputField = $this->Prepend . '
<div id="PermissionItemTemplate" style="display:none;">
	<div class="PermissionItemRow PermissionItemRow_Flag_%%flag%%" id="PermissionItemRow_%%permid%%">
		<input type="checkbox" class="PermissionItemCheckbox" id="PermissionItemCheckbox_%%permid%%" value="%%permid%%" />
		<label for="PermissionItemCheckbox_%%permid%%">%%permission%%%%granularity%% <a href="#" onclick="editPermissionItem(\'%%permid%%\');return false;"><img src="images/configure.png" alt="' . iwp_htmlentities($this->lang->Get('Edit')) . '" /></a></label>
	</div>
</div>

<div id="PermissionItemList" ' . $this->GetAttributes() . '>
	<div id="PermissionItemListEmpty" style="display:none;">' . iwp_htmlentities($this->lang->Get('EmptyPermissionList')) . '</div>
</div>

<input type="button" class="FormButton" onclick="return addNewPermissionClick()" value="' . iwp_htmlentities($this->lang->Get('AddNewGroupPermission')) . '" />
<input type="button" class="FormButton" onclick="return deleteSelectedPermissionClick()" value="' . iwp_htmlentities($this->lang->Get('DeleteSelected')) . '" /><br />
' . $this->Append;

		parent::GetFieldOutput();

		$this->template->Assign('inputField', $inputField);
		$this->template->Assign('FieldName', $this->FieldName);

		if(!$setOnly){
			return $this->template->ParseTemplate('form.field', true);
		}
		return '';
	}

	/**
	 * Validate
	 * This is the function that data for this field is passed to to ensure it was submitted properly.
	 *
	 * @return string|boolean If the data is not valid, it will return false, if it is valid it will return a value
	 */
	public function Validate($arrData){
		return $arrData[$this->FieldName];
	}

}

/**
* Interface to be implemented by various actions in this module's mini framework
*
* @package IWP_Modules
* @subpackage Calendar
*/
interface iwp_module_calendar_action_interface {

	/**
	* Function to be routed to by the action-handling framework
	*
	*/
	public function handleAction ();
}

/**
* Base action class used by this module's mini framework, extended on by more specific action handlers
*
* @package IWP_Modules
* @subpackage Calendar
*/
class iwp_module_calendar_action extends iwp_base {

	/**
	 *
	 * @var iwp_module_calendar
	 */
	protected $module;

	public function __construct (iwp_module_calendar $module) {
		$this->module = $module;
	}
}

/**
*
* @package IWP_Modules
* @subpackage Calendar
*/
class iwp_module_calendar_adminaction extends iwp_module_calendar_action {

	public function handleAction ($dialog = false) {
		if (!$dialog) {
			$this->module->addAdminResources();
			$this->module->displaySessionTemplateMessages();

			$this->template->Assign('BreadCrumbSection', $this->module->lang->Get('BreadCrumbSection'));
			$this->template->Assign('BreadCrumbAction', '');
		}

		$data = array();
		$data['lang'] = $this->module->lang->GetLangVars();
		$this->template->Assign($this->module->moduleName, $data, false);

		//	admin language js

		$this->template->AddLanguageForJS(array(
			'Starts',
			'Ends',
			'Location',
			'more',
			'Editthisevent',
			'Deletethisevent',
			'ConfirmDelete',
			'Today',
			'ViewName_Day',
			'ViewName_Week',
			'ViewName_Month',
			'ViewName_Tinymonth',
			'NoPermissionEventEdit',
			'NoPermissionEventDelete',
			'NoPermissionEventCreate',
			'SubmittingPleaseWait',
			'EventColor',
			'CreateEvent',
			'or',
			'AddMoreDetails',
			'Calendar_Summary',
			'Alldayeventfor',
			'ClosePopup',
			'Closeandapplycolor',
			'Bookmarkthiscolor',
		), $this->module->lang, 'module_calendar_');
	}
}

/**
* Base class used by all 'remote' actions (actions usually triggered by form submits or ajax requests)
*
* @package IWP_Modules
* @subpackage Calendar
*/
class iwp_module_calendar_remoteaction extends iwp_module_calendar_action {

	/**
	 *
	 * @var array
	 */
	protected $parameters;

	/**
	 *
	 * @var string
	 */
	protected $extension;

	/**
	* put your comment there...
	*
	* @param iwp_module_calendar $module
	* @param array $parameters
	* @param string $extension
	* @return iwp_module_calendar_remoteaction
	*/
	public function __construct (iwp_module_calendar $module, $parameters, $extension) {
		$this->parameters = $parameters;
		$this->extension = $extension;
		parent::__construct($module);
	}

	/**
	 *
	 * @param DateTime $date
	 * @return string DateTime formatted as YYYYMMDD[HHMM] - The time is dropped off if it is 00:00
	 */
	public static function formatDateJSON (DateTime $date) {
		$string = $date->format(iwp_module_calendar::JSON_DATETIME_FORMAT);	//	the client js is programmed to recognise a compressed date format
		return preg_replace('/0{4}$/', '', $string);						//	drop any '0000' time from the data packet, the client js will assume midnight if it's not present
	}

	public static function outputJavascript (&$data) {
		$charset = iwp_config::Get('charset');
		if (!$charset) {
			$charset = 'utf-8';
		}

		header('Content-type: application/x-javascript; charset=' . $charset);
		echo '{}&& ';	//	json prefix - http://trac.dojotoolkit.org/ticket/6380
		echo $data;
	}

	public static function outputJSON (&$data) {
		$javascript = json_encode($data);
		self::outputJavascript($javascript);
	}
}

/**
* Base class used by all 'remote' actions in the admin (actions usually triggered by form submits or ajax requests)
*
* @package IWP_Modules
* @subpackage Calendar
*/
class iwp_module_calendar_adminremoteaction extends iwp_module_calendar_remoteaction { }

/**
* This class handles the CalendarDisplayOptions admin remote action, displaying the Display Options form for when a gallery is inserted as a block or as part of tinymce content
*
* @package IWP_Modules
* @subpackage Calendar
*/
class iwp_module_calendar_adminremotecalendardisplayoptionsaction extends iwp_module_calendar_adminremoteaction {

	public function handleAction () {
		if (isset($this->parameters['blockId'])) {
			$blockId = (int)$this->parameters['blockId'];
			$data = $this->parameters['data'];

			$blockdata = unserialize($data['content']);
		} else {
			$blockdata = array(
				'views'	=> array('day', 'week', 'month'),
				'first' => 'month',
				'title' => '',
			);
		}

		$this->template->Assign('blockdata', $blockdata);

		$vars = array();
		$vars['lang'] = $this->module->lang->GetLangVars();
		$this->module->template->Assign($this->module->moduleName, $vars, false);

		$this->template->ParseTemplate('block.displayoptions', false, $this->module->moduleName);
	}
}

/**
* This class handles the EventDelete admin remote action, deleting an event if the admin permissions are correct
*
* @package IWP_Modules
* @subpackage Calendar
*/
class iwp_module_calendar_adminremoteeventdeleteaction extends iwp_module_calendar_adminremoteaction implements iwp_module_calendar_action_interface {

	public function handleAction () {

		$eventid = (int)@$_POST['id'];

		if (!$this->module->hasPerm('eventdelete', $eventid)) {
			$this->xml->writeElement('status', 0);
			$this->xml->writeElement('message', $this->module->lang->Get('NoPermissionEventDelete'));
			$this->xml->outputXML();
			die();
		}

		$event = new iwp_module_calendar_event();
		if (!$event->Load($eventid)) {
			$this->xml->writeElement('status', 0);
			$this->xml->writeElement('message', $this->module->lang->Get('InvalidEventID'));
			$this->xml->outputXML();
			die();
		}

		$summary = $event->Get('summary');

		if (!$event->Delete()) {
			$this->xml->writeElement('status', 0);
			$this->xml->writeElement('message', $this->module->lang->Get('ContentDeleteError'));
			$this->xml->outputXML();
			die();
		}

		iwp_activity::AddEntry('module', 'calendar', 'eventdelete', $eventid, $summary);
		iwp_module_calendar::flushOccurrenceCache();

		$this->xml->writeElement('status', 1);
		$this->xml->writeElement('message', $this->module->lang->Get('ContentDeleteSuccess'));
		$this->xml->outputXML();
	}
}

/**
 * Implements the saving of new and edited calendar events
 *
 * @package IWP_Modules
 * @subpackage Calendar
 */
class iwp_module_calendar_adminremoteeventeditsaveaction extends iwp_module_calendar_adminremoteaction implements iwp_module_calendar_action_interface {

	public function handleAction () {

		$event = new iwp_module_calendar_event();
		$eventid = intval(@$_POST['eventid']);

		$editing = false;

		//	editing or creating?
		if ($eventid) {
			//	editing
			$editing = true;
			$event->Load($eventid);
			$formAction = new iwp_module_calendar_admineventeditaction($this->module);

			if (!$this->module->hasPerm('eventedit', $eventid)) {
				$this->xml->writeElement('status', 0);
				$this->xml->writeElement('error', $this->module->lang->Get('NoPermissionEventEdit'));
				$this->xml->outputXML();
				die();
			}
		} else {
			//	creating
			$formAction = new iwp_module_calendar_admineventcreateaction($this->module);

			if (!$this->module->hasPerm('eventcreate')) {
				$this->xml->writeElement('status', 0);
				$this->xml->writeElement('error', $this->module->lang->Get('NoPermissionEventCreate'));
				$this->xml->outputXML();
				die();
			}
		}

		//	although the client side form has been replaced, still use server side form class for validation
		$form = $formAction->getForm();

		$data = $_POST;

		//	@todo this is a hack, I don't like modifying post data, don't know how else to do it in iwp field framework - merge the datetime post fields back into one field for the $form->Validate call
		$data['startdatetime'] = trim($data['startdatetime_date'] . ' ' . @$data['startdatetime_time']);
		$data['enddatetime'] = trim($data['enddatetime_date'] . ' ' . @$data['enddatetime_time']);

		//	form class validation
		$valid = $form->Validate($data, $errorMessages, $errorFields);

		if ($errorMessages === null) {
			$errorMessages = array();
		}

		if ($errorFields === null) {
			$errorFields = array();
		}

		//	validation specific to calendar module

		//	validate that the time has been provided if all-day is not checked
		if (!isset($data['allday'])) {
			$data['allday'] = false;

			//	event is not all day, so a starting time is required
			if (!@$data['startdatetime_time']) {
				$valid = false;
				$errorFields[] = 'startdatetime';
				$errorMessages[] = sprintf($this->lang->Get('ValidError_IsNotBlank'), $this->module->lang->Get('field_startdatetime_Name'));
			} else if (!@$data['enddatetime_time']) {
				//	starting time provided but no ending time provided; the field is not required, so use the starting time as ending time
				$data['enddatetime_time'] = $data['startdatetime_time'];
			}
		} else {
			$data['allday'] = true;

			//	wipe out any provided time values if the event is all day
			$data['startdatetime_time'] = '';
			$data['enddatetime_time'] = '';
		}

		//	validate start date/time and translate into database storable value
		$data['startdatetime'] = '';
		if (!in_array('startdatetime', $errorFields)) {

			if (!$this->valid->ParseDateString($data['startdatetime_date'], $parts)) {
				$valid = false;
				$errorFields[] = 'startdatetime';
				$errorMessages[] = sprintf($this->module->lang->Get('ValidError_IsNotValidDate'), $this->module->lang->Get('field_startdatetime_Name'));
			} else {
				$data['startdatetime'] .= $parts['year'] . str_pad($parts['month'], 2, '0', STR_PAD_LEFT) . str_pad($parts['day'], 2, '0', STR_PAD_LEFT);
			}

			if (!$data['allday']) {
				//	validate the provided time

				if (!$this->valid->ParseTimeString($data['startdatetime_time'], $parts)) {
					$valid = false;
					$errorFields[] = 'startdatetime';
					$errorMessages[] = sprintf($this->module->lang->Get('ValidError_IsNotValidTime'), $this->module->lang->Get('field_startdatetime_Name'));
				} else {
					$data['startdatetime'] .= str_pad($parts['hour'], 2, '0', STR_PAD_LEFT) . str_pad($parts['minute'], 2, '0', STR_PAD_LEFT);
				}
			}
		}

		//	validate end date/time and translate into database storable value
		$data['enddatetime'] = '';
		if (!in_array('enddatetime', $errorFields)) {

			if (!$this->valid->ParseDateString($data['enddatetime_date'], $parts)) {
				$valid = false;
				$errorFields[] = 'enddatetime';
				$errorMessages[] = sprintf($this->module->lang->Get('ValidError_IsNotValidDate'), $this->module->lang->Get('field_enddatetime_Name'));
			} else {
				$data['enddatetime'] = $parts['year'] . str_pad($parts['month'], 2, '0', STR_PAD_LEFT) . str_pad($parts['day'], 2, '0', STR_PAD_LEFT);
			}

			if (!$data['allday']) {
				//	validate the provided time

				if (!$this->valid->ParseTimeString($data['enddatetime_time'], $parts)) {
					$valid = false;
					$errorFields[] = 'enddatetime';
					$errorMessages[] = sprintf($this->module->lang->Get('ValidError_IsNotValidTime'), $this->module->lang->Get('field_enddatetime_Name'));
				} else {
					$data['enddatetime'] .= str_pad($parts['hour'], 2, '0', STR_PAD_LEFT) . str_pad($parts['minute'], 2, '0', STR_PAD_LEFT);
				}
			}
		}

		//	validate that the end and start dates make sense (they've already been validated as valid dates by ParseDateString calls above)
		if (!in_array('startdatetime', $errorFields) && !in_array('enddatetime', $errorFields)) {
			//	recurrence rule backend expects events to have a duration, so all end dates must be > the start date
			//	except for all-day events where the date provided in the form is allowed tobe 20090101 - 20090101 for example, because we'll actually save it as 20090101 - 20090102

			$data['startdatetime_object'] = new DateTime($data['startdatetime']);
			$data['enddatetime_object'] = new DateTime($data['enddatetime']);

			if ($data['allday'] && $data['enddatetime_object'] < $data['startdatetime_object'] || !$data['allday'] && $data['enddatetime_object'] <= $data['startdatetime_object']) {
				$valid = false;
				$errorFields[] = 'enddatetime';
				$errorMessages[] = sprintf($this->module->lang->Get('ValidError_EndTimeStartTime'));
			}
		}


		//	if custom repeat type was selected, need to validate the values inside the hidden form
		/*
		if (@$data['repeat_type'] == 'custom') {

			//	interval is used by all custom repeat types; validate it always
			$data['repeat_custom_interval'] = intval($data['repeat_custom_interval']);
			if ($data['repeat_custom_interval'] < 1) {
				$valid = false;
				$errorFields[] = 'repeat_custom_interval';
				$errorMessages[] = $this->module->lang->Get('ValidError_InvalidInterval');
			}

			//	range is used by all custom repeat types; validate it always
			if ($data['repeat_custom_range'] == 'none') {
				//	nothing to validate

			} else if ($data['repeat_custom_range'] == 'until') {
				//	validate the provided 'until' date
				if (!$this->valid->ParseDateString($data['repeat_custom_range_until'], $parts)) {
					$valid = false;
					$errorFields[] = 'repeat_custom_range_until';
					$errorMessages[] = sprintf($this->module->lang->Get('ValidError_IsNotValidDate'), $this->module->lang->Get('field_repeat_custom_range_until_Name'));
				} else {
					$data['repeat_custom_range_until'] = $parts['year'] . str_pad($parts['month'], 2, '0', STR_PAD_LEFT) . str_pad($parts['day'], 2, '0', STR_PAD_LEFT);
				}

			} else {
				$valid = false;
				$errorFields[] = 'repeat_custom_range';
				$errorMessages[] = sprintf($this->module->lang->Get('ValidError_RepeatCustomRange'), $data['repeat_custom_range']);
			}

			if ($data['repeat_custom_type'] == 'daily') {
				//	nothing to validate

			} else if ($data['repeat_custom_type'] == 'weekly') {
				//	validate the first day of the week
				if (!is_numeric($data['repeat_custom_week_fdotw']) || (int)$data['repeat_custom_week_fdotw'] < 0 || (int)$data['repeat_custom_week_fdotw'] > 6) {
					$valid = false;
					$errorFields[] = 'repeat_custom_week_fdotw';
					$errorMessages[] = $this->module->lang->Get('ValidError_RepeatCustomWeekFDOTW');
				} else {
					$data['repeat_custom_week_fdotw'] = (int)$data['repeat_custom_week_fdotw'];
				}

				//	validate the byweekday selection
				if (!isset($data['repeat_custom_week'])) {
					//	blank arrays are fine as this indicates that the weekday should be taken from the event start date
					$data['repeat_custom_week'] = array();
				} else if (!is_array($data['repeat_custom_week'])) {
					//	should always be an array
					$valid = false;
					$errorFields[] = 'repeat_custom_week';
					$errorMessages[] = $this->module->lang->Get('ValidError_RepeatCustomWeek');
				} else {
					$data['repeat_custom_week'] = array_unique($data['repeat_custom_week']);

					//	array_diff will return elements from array 1 that do not exist in expected array 2
					$invalidElements = array_diff($data['repeat_custom_week'], array('MO','TU','WE','TH','FR','SA','SU'));
					if (count($invalidElements)) {
						$valid = false;
						$errorFields[] = 'repeat_custom_week';
						$errorMessages[] = $this->module->lang->Get('ValidError_RepeatCustomWeek');
					}
				}

			} else if ($data['repeat_custom_type'] == 'monthly') {

				if ($data['repeat_custom_monthly_type'] == 'bysetpos') {

					$invalidElements = @array_diff(array(@$data['repeat_custom_monthly_bysetpos_position']), array('1','2','3','4','5','-1'));

				} else if ($data['repeat_custom_monthly_type'] == 'bymonthday') {

					//	validate the bymonthday selection
					if (!isset($data['repeat_custom_monthly_bymonthday'])) {
						//	blank arrays are fine as this indicates that the monthday should be taken from the event start date
						$data['repeat_custom_monthly_bymonthday'] = array();

					} else if (!is_array($data['repeat_custom_monthly_bymonthday'])) {
						//	should always be an array
						$valid = false;
						$errorFields[] = 'repeat_custom_monthly_bymonthday';
						$errorMessages[] = $this->module->lang->Get('ValidError_RepeatCustomMonthlyBymonthday');
					} else {
						$data['repeat_custom_monthly_bymonthday'] = array_unique($data['repeat_custom_monthly_bymonthday']);

						//	array_diff will return elements from array 1 that do not exist in expected array 2
						$invalidElements = array_diff($data['repeat_custom_monthly_bymonthday'], array('1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24','25','26','27','28','29','30','31'));
						if (count($invalidElements)) {
							$valid = false;
							$errorFields[] = 'repeat_custom_monthly_bymonthday';
							$errorMessages[] = $this->module->lang->Get('ValidError_RepeatCustomMonthlyBymonthday');
						}
					}
				} else {

					$valid = false;
					$errorFields[] = 'repeat_custom_monthly_type';
					$errorMessages[] = sprintf($this->module->lang->Get('ValidError_RepeatCustomMonthlyType'), $data['repeat_custom_monthly_type']);
				}

			} else if ($data['repeat_custom_type'] == 'yearly') {

			} else {
				$valid = false;
				$errorFields[] = 'repeat_custom_type';
				$errorMessages[] = sprintf($this->module->lang->Get('ValidError_RepeatCustomType'), $data['repeat_custom_type']);
			}
		} else
		*/

		if (@$data['repeat_type'] == 'daily') {
			//	translate into daily repeat rule
			$data['rrule'] = iwp_module_calendar::RRULE_STANDARD_DAILY;

		} else if (@$data['repeat_type'] == 'weekly') {
			//	translate into weekly repeat rule
			$data['rrule'] = iwp_module_calendar::RRULE_STANDARD_WEEKLY;

		} else if (@$data['repeat_type'] == 'monthly') {
			//	translate into monthly repeat rule
			$data['rrule'] = iwp_module_calendar::RRULE_STANDARD_MONTHLY;

		} else if (@$data['repeat_type'] == 'yearly') {
			//	translate into yearly repeat rule
			$data['rrule'] = iwp_module_calendar::RRULE_STANDARD_YEARLY;

		} else if (@$data['repeat_type'] == 'none') {
			$data['rrule'] = '';

		} else {
			//	no valid repeat type selected
			$valid = false;
			$errorFields[] = 'repeat_type';
			$errorMessages[] = sprintf($this->module->lang->Get('ValidError_RepeatType'), @$data['repeat_type']);
		}

		//	send error message if validation failed
		if (!$valid) {
			$this->xml->writeElement('status', 0);

			$this->xml->startElement('error');
			$this->xml->writeAttribute('html', 'true');
			$this->xml->writeCdata(sprintf(iwp_language::getInstance()->Get('ContentSaveErrors') . '<ul><li>' . implode('</li><li>', $errorMessages) . '</li></ul>', $this->module->lang->Get('event')));
			$this->xml->endElement();

			foreach ($errorFields as $errorField) {
				$this->xml->startElement('field');
				$this->xml->writeAttribute('name', $errorField);
				$this->xml->writeAttribute('highlight', 'true');
				$this->xml->endElement();
			}

			$this->xml->outputXML();
			die();
		}

		if ($editing) {
			//	fields that are set only when editing
			$event->Set('sequence', (int)$event->Get('sequence') + 1);	//	increment the sequence counter, which is an internal 'edits' counter used in ical

		} else {
			//	fields that are set only when creating
			$event->Set('uid', iwp_module_calendar_event::generateUid());
			$event->Set('sequence', 0);
			$event->Set('created', gmdate('YmdHis'));

		}

		if (preg_match('#^[A-Fa-f0-9]{6}$#', @$data['color']) > 0) {
			$event->Set('color', $data['color']);
		} else if (!$editing) {
			//	if creating, generate a random colour for one not provided
			$event->Set('color', iwp_module_calendar_event::getRandomColor());
		}

		//	basic editable fields
		$event->Set('summary', $data['summary']);
		$event->Set('description', @$data['description']);
		$event->Set('location', @$data['location']);
		$event->Set('rrule', $data['rrule']);

		//	date/time fields that should have been formatted properly by the above validation (into either YYYYMMDD or YYYYMMDDHHMM values)
		$event->Set('dtstart', $data['startdatetime']);

		if ($data['allday']) {
			//	all-day events must be stored as form day + 1 because date calculations are performed on a range-start >= now > range-end basis, so this creates an end date that is equivalent to 20090101 23:59 for example
			$date = $data['enddatetime_object'];
			$date->modify('1 day');
			$event->Set('dtend', $date->format('Ymd'));
		} else {
			$event->Set('dtend', $data['enddatetime']);
		}

		$eventId = $event->Save();
		iwp_module_calendar::flushOccurrenceCache();

		if ($eventId === false) {
			$this->xml->writeElement('status', 0);
			$this->xml->writeElement('error', sprintf(iwp_language::getInstance()->Get('ContentSaveErrors') . '<ul><li>' . $this->module->lang->Get('ContentSaveError') . '</li><li>' . $event->GetError() . '</li></ul>', $this->module->lang->Get('event')));
			$this->xml->outputXML();
			die();
		}

		if ($editing) {
			iwp_activity::AddEntry('module', 'calendar', 'eventedit', $eventId, $event->Get('summary'));
		} else {
			iwp_activity::AddEntry('module', 'calendar', 'eventcreate', $eventId, $event->Get('summary'));
		}

		$this->xml->writeElement('status', 1);
		$this->xml->writeElement('eventid', $eventId);
		$this->xml->writeElement('message', $this->module->lang->Get('EventSavedSuccessfully'));
		$this->xml->writeElement('updatedata', 1);

		if (@$_POST['savemethod'] == 'andexit') {
			$this->xml->writeElement('closemodal', 1);
		}

		$this->xml->outputXML();
	}
}

/**
 * Implements the display of the admin calendar event creation form
 *
 * @package IWP_Modules
 * @subpackage Calendar
 */
class iwp_module_calendar_admineventcreateaction extends iwp_module_calendar_adminaction implements iwp_module_calendar_action_interface {

	/**
	 *
	 * @return iwp_form
	 */
	public function getForm () {
		$form = new iwp_form();
		//$form->AddGroup($this->module->lang->Get('EditFormGroupEventDetails'));

		$form->AddField('hidden', 'eventid', null, $this->module->lang);
		$form->AddField('hidden', 'color', null, $this->module->lang);

		$form->AddField('textbox', 'summary', null, $this->module->lang)
			->DisableLabel()
			->DisableHelpTip()
			->AddValidation('IsNotBlank')
			->SetAttribute('class', 'Field250')
			->Required = true;

		$form->AddField('checkbox', 'allday', null, $this->module->lang)
			->DisableHelpTip();

		$form->AddField('datetime', 'startdatetime', null, $this->module->lang)
			->DisableHelpTip()
			->AddValidation('IsNotBlank')
			->Required = true;

		$form->AddField('datetime', 'enddatetime', null, $this->module->lang)
			->DisableHelpTip()
			->AddValidation('IsNotBlank')
			->Required = true;

		$form->AddField(array('module_custom:repeat', 'calendar'), 'repeat', null, $this->module->lang);

		$form->AddField('textbox', 'location', null, $this->module->lang)
			->DisableLabel()
			->SetAttribute('class', 'Field250')
			->DisableHelpTip();

		$field = $form->AddField('wysiwyg', 'description', null, $this->module->lang)
			->DisableLabel()
			->DisableHelpTip()
			->AddValidation('GetWYSIWYGContent');

		$field->editor->buttons1 = 'undo,redo,|,bold,italic,underline,|,cut,copy,paste,pastetext,|,link,unlink,iwpinsertlink,|,forecolor,backcolor,|,code';
		$field->editor->buttons2 = '';
		$field->editor->buttons3 = '';
		$field->editor->buttons4 = '';
		$field->editor->horizontalResizing = false;

		$field->editor->width = 430;

		return $form;
	}

	public function handleAction () {
		//	@todo create permission check

		parent::handleAction(true);

		$templateData = array();

		//
		//	populate the form with creation defaults based on the request

		//	adopt start date from query string or use now

		$allDay = true;

		if (preg_match('#^\d{8}$#', @$_GET['eventdtstart'])) {
			$dtstart = new InterspireICalendarDateTime($_GET['eventdtstart'] . date('H', time()) . '00');
		} else if (preg_match('#^\d{12}$#', @$_GET['eventdtstart'])) {
			$allDay = false;
			$dtstart = new InterspireICalendarDateTime($_GET['eventdtstart']);
		} else {
			$dtstart = new InterspireICalendarDateTime();
		}

		//	adopt end date from query string or use start + 30 mins

		if (preg_match('#^\d{8}$#', @$_GET['eventdtend'])) {
			$dtend = new InterspireICalendarDateTime($_GET['eventdtend'] . date('H', time() + 3600) . '00');
		} else if (preg_match('#^\d{12}$#', @$_GET['eventdtend'])) {
			$allDay = false;
			$dtend = new InterspireICalendarDateTime($_GET['eventdtend']);
		} else {
			$dtend = clone($dtstart);
			$dtend->modify('30 minutes');
		}

		$templateData['allday'] = $allDay;

		//	apply date and time

		$dateFormat = iwp_settings::IsAmericanDateFormat(iwp_getShortDateFormat()) ? 'm/d/Y' : 'd/m/Y';	//	date format which is usable by the date picker

		$templateData['startdatetime_date'] = $dtstart->format($dateFormat);
		$templateData['startdatetime_time'] = $dtstart->format('g:i A');

		$templateData['enddatetime_date'] = $dtend->format($dateFormat);
		$templateData['enddatetime_time'] = $dtend->format('g:i A');

		//	other defaults

		$templateData['summary'] = @$_GET['eventsummary'];
		$templateData['color'] = @$_GET['eventcolor'];

		//	render form html, js, etc.

		$templateData['lang'] = $this->module->lang->GetLangVars();
		$this->template->Assign('calendar', $templateData);

		//	display page

		$this->template->ParseTemplate('calendar.event.edit', false, 'calendar');
	}
}

/**
 * Implements the display of the admin calendar event edit form by extending on the create action
 *
 * @package IWP_Modules
 * @subpackage Calendar
 */
class iwp_module_calendar_admineventeditaction extends iwp_module_calendar_admineventcreateaction implements iwp_module_calendar_action_interface {

	public function handleAction () {
		//
		//	populate the form with event values from database

		$eventId = (int)@$_GET['eventid'];
		$event = new iwp_module_calendar_event();
		if (!$event->Load($eventId)) {
			throw new Exception('Invalid Event ID');
		}

		$templateData = array();

		$templateData['eventid'] = $eventId;
		$templateData['summary'] = $event->Get('summary');
		$templateData['location'] = $event->Get('location');
		$templateData['description'] = $event->Get('description');
		$templateData['repeat'] = $event->Get('rrule');
		$templateData['color'] = $event->Get('color');

		//	adopt start date from event data or use now
		$allDay = true;

		if (preg_match('#^\d{8}$#', $event->Get('dtstart'))) {
			$dtstart = new InterspireICalendarDateTime($event->Get('dtstart') . date('H', time()) . '00');
		} else if (preg_match('#^\d{12}$#', $event->Get('dtstart'))) {
			$allDay = false;
			$dtstart = new InterspireICalendarDateTime($event->Get('dtstart'));
		} else {
			$dtstart = new InterspireICalendarDateTime();
		}

		//	adopt end date from event data or use start + 30 mins
		if (preg_match('#^\d{8}$#', $event->Get('dtend'))) {
			$dtend = new InterspireICalendarDateTime($event->Get('dtend') . date('H', time() + 3600) . '00');
		} else if (preg_match('#^\d{12}$#', $event->Get('dtend'))) {
			$allDay = false;
			$dtend = new InterspireICalendarDateTime($event->Get('dtend'));
		} else {
			$dtend = clone($dtstart);
			$dtend->modify('30 minutes');
		}

		$templateData['allday'] = $allDay;

		if ($allDay) {
			//	all-day end dates are stored in the database as day + 1 for calculation purposes
			$dtend->modify('-1 day');
		}

		$dateFormat = iwp_settings::IsAmericanDateFormat(iwp_getShortDateFormat()) ? 'm/d/Y' : 'd/m/Y';	//	date format which is usable by the date picker


		$templateData['startdatetime_date'] = $dtstart->format($dateFormat);
		$templateData['startdatetime_time'] = $dtstart->format('g:i A');
		$templateData['enddatetime_date'] = $dtend->format($dateFormat);
		$templateData['enddatetime_time'] = $dtend->format('g:i A');

		//	render form html, js, etc.

		$templateData['lang'] = $this->module->lang->GetLangVars();
		$this->template->Assign('calendar', $templateData);

		//	display page

		$this->template->ParseTemplate('calendar.event.edit', false, 'calendar');
	}
}

/**
 * Implements the admin calendar view
 *
 * @package IWP_Modules
 * @subpackage Calendar
 */
class iwp_module_calendar_adminviewaction extends iwp_module_calendar_adminaction implements iwp_module_calendar_action_interface {

	protected function getViewList () {
		$serverDate = new DateTime();

		$events = iwp_module_calendar_event::getInstance()->LoadInstances();
		iwp_module_calendar_event::sortEvents($events);

		$results = array();
		foreach ($events as $event) {
			$result = array();

			$result['id'] = $event->GetId();
			$result['summary'] = $event->Get('summary');

			$dtstart = $event->dtstart();
			$result['startdate'] = $dtstart->format(iwp_config::Get('ShortDateFormat') . ' ' . iwp_module_calendar::TIME_FORMAT);
			$result['startdate_tz'] = $event->Get('dtstart_tz');

			if (!$result['startdate_tz']) {
				$result['startdate_utc_ts'] = $dtstart->format('Y-m-d\TH:i:s');
			}

			$dtstart->setTimezone($serverDate->getTimezone());
			$result['startdate_server'] = $dtstart->format(iwp_config::Get('ShortDateFormat') . ' ' . iwp_module_calendar::TIME_FORMAT);

			$dtstart->setTimezone(new DateTimeZone('UTC'));
			$result['startdate_utc'] = $dtstart->format(iwp_config::Get('ShortDateFormat') . ' ' . iwp_module_calendar::TIME_FORMAT);

			if ($result['startdate_tz']) {
				$result['startdate_utc_ts'] = $dtstart->format('Y-m-d\TH:i:s') . 'Z';
			}

			$result['rrule'] = $event->Get('rrule');

			$result['can_anyaction'] = true;

			$results[] = $result;
		}
		return $results;
	}

	public function handleAction () {
		//	@todo view permission check

		parent::handleAction();

		//$this->template->Assign('ViewList', $this->getViewList($this->module));

		$editor = new tiny_mce();
		$editor->mode = 'none';
		$editor->buttons1 = 'undo,redo,|,bold,italic,underline,|,cut,copy,paste,pastetext,|,link,unlink,iwpinsertlink,|,forecolor,backcolor,|,code';
		$editor->buttons2 = '';
		$editor->buttons3 = '';
		$editor->buttons4 = '';
		$editor->horizontalResizing = false;
		$editor->width = 430;

		$this->template->Assign('editorCode', $editor->GetEditorHTML('blockhtml'));

		$this->template->Assign(array('calendar', 'american'), iwp_settings::IsAmericanDateFormat(iwp_getShortDateFormat()), false);

		$this->template->Assign('PageTitle', $this->module->lang->Get('PageTitle'));
		$this->template->Assign('PageIntro', $this->module->lang->Get('PageIntro'));

		$this->template->ParseTemplate('header');
		$this->template->ParseTemplate('calendar.view', false, 'calendar');
		$this->template->ParseTemplate('footer');
	}
}

/**
 * Handle a request for occurrence information from admin users
 *
 * @package IWP_Modules
 * @subpackage Calendar
 */
class iwp_module_calendar_adminremoteoccurrencesaction extends iwp_module_calendar_adminremoteaction implements iwp_module_calendar_action_interface {

	public function handleAction () {
		//	load the front end occurrences handler and pass-through to it, telling it to return data instead of output

		$frontEndAction = new iwp_module_calendar_remoteoccurrencesaction($this->module, array(@$_GET['range']), 'json');
		$data = $frontEndAction->handleAction(true);

		//	process the data and add control panel permission information to the output

		foreach ($data['e'] as &$event) {
			//	control panel edit
			$event['ce'] = $this->module->hasPerm('eventedit', $event['i']);

			//	control panel delete
			$event['cd'] = $this->module->hasPerm('eventdelete', $event['i']);
		}

		$this->outputJSON($data);
		die();
	}
}

/**
 * Handle a request for occurrence information from front end users
 *
 * @package IWP_Modules
 * @subpackage Calendar
 */
class iwp_module_calendar_remoteoccurrencesaction extends iwp_module_calendar_remoteaction implements iwp_module_calendar_action_interface {

	public function handleAction ($return = false) {

		//	--- VALIDATE AND PARSE INPUT

		if (!preg_match('/^(?P<year>[\d]{4})(?P<month>[\d]{2})?$/', $this->parameters[0], $range)) {
			throw new Exception('Invalid URL');
		}

		$range['year'] = intval($range['year']);
		if ($range['year'] < 1900 || $range['year'] >= 2100) {
			//	safety check, 1900-2099 is the selectable date range in windows so may as well limit queries to this range
			//	(high year numbers can generate long-running calculations since repeating events are calculated from their start date)
			throw new Exception('Invalid URL');
		}

		if (isset($range['month'])) {
			$range['month'] = intval($range['month']);
			if ($range['month'] < 1 || $range['month'] > 12) {
				//	month is too low or too high
				throw new Exception('Invalid URL');
			}

			$rangeBegin = $range['year'] . '-' . $range['month'] . '-01';
			$rangeScale = '1 month';
		} else {
			//	no month specified, assume a year range
			$rangeBegin = $range['year'] . '-01-01';
			$rangeScale = '1 year';
		}

		//	--- FETCH AND SORT DATA
		$now = new iwp_module_calendar_datetime();
		$timezone = $now->getTimezone();
		$timezoneIdentifier = $timezone->getName();
		$cacheBlock = $this->parameters[0];	//	parameters[0] has been validated above

		$cacheRow = $this->db->FetchQuery("SELECT data FROM " . GetConfig('tablePrefix') . "module_calendar_occurrencecache WHERE cacheblock = '" . $cacheBlock . "' AND tz = '" . $this->db->Quote($timezoneIdentifier) . "'");

		if ($cacheRow) {
			$data = unserialize($cacheRow['data']);

			if ($return) {
				return $data;
			}

			self::outputJSON($data);
			die();
		}

		//	create a date range based on the session, cookie or config specified time
		$rangeBegin = new iwp_module_calendar_datetime($rangeBegin);

		$rangeEnd = clone($rangeBegin);
		$rangeEnd->modify($rangeScale);

		$event = iwp_module_calendar_event::getInstance();

		//	json data uses abbreviations for smaller data packets
		$data = array(
			'k' => $cacheBlock,	//	the range that this is in response to
			'e' => array(),					//	event list
		);

		//	@todo	needs to be optimised to only load repeating instances that end after the given timeframe, and non-repeating instances within the given timeframe, instead of all repeating instances
		$events = $event->LoadInstances();

		foreach ($events as $event) {
			$occurrences = $event->occurrences($rangeBegin, $rangeEnd);
			if (empty($occurrences)) {
				//	event does not have occurrences in this time range, exclude it from output
				continue;
			}

			$dtstart = $event->dtstart();
			$dtstart->setTimezone($timezone);

			$dtend = $event->dtend();
			$dtend->setTimezone($timezone);

			//	format this event and it's occurrences into a small json packet
			$json_occurrences = array();
			foreach ($occurrences as $occurrence) {
				//	adjust the occurence timezone to match the range
				$occurrence->setTimezone($timezone);

				$json_occurrences[] = self::formatDateJSON($occurrence);
			}

			$eventPacket = array(
				'i'		=> $event->GetId(),					//	id
				's'		=> $event->Get('summary'),			//	summary
				'l'		=> $event->Get('location'),			//	location
				'd'		=> $event->Get('description'),		//	description
				'c'		=> $event->Get('color'),			//	color
				'ds'	=> self::formatDateJSON($dtstart),	//	event start
				'de'	=> self::formatDateJSON($dtend),	//	event end
				'o'		=> $json_occurrences,				//	occurrence list in queried range
			);

			//	add it to the json packet output
			$data['e'][] = $eventPacket;
		}

		$this->db->InsertQuery(GetConfig('tablePrefix') . 'module_calendar_occurrencecache', array(
			'cacheblock'	=> $cacheBlock,
			'tz'			=> $timezoneIdentifier,
			'data'			=> serialize($data),
		));

		if ($return) {
			return $data;
		}

		$json = json_encode($data);

		self::outputJSON($data);
	}
}

/**
 * Handles a request for parsing and returning a template from this module's template folder - was going to be used for streaming view templates to the calendar front end but may no longer be used
 *
 * @package IWP_Modules
 * @subpackage Calendar
 */
class iwp_module_calendar_remotetemplateaction extends iwp_module_calendar_remoteaction implements iwp_module_calendar_action_interface {

	public function getRequestedTemplateName () {
		return $this->parameters[0];
	}

	public function handleAction () {
		if ($this->extension != 'html') {
			$this->controller->ShowPage('NotFound');
			die();
		}

		$template = $this->getRequestedTemplateName();

		try {
			$charset = iwp_config::Get('charset');
			if (!$charset) {
				$charset = 'utf-8';
			}

			header('Content-type: text/html; charset=' . $charset);
			$this->template->ParseTemplate($template, false, $this->module->moduleName);
			die();
		} catch (InterspireTemplateNothingToParseException $exception) {
			$this->controller->ShowPage('NotFound');
			die();
		}
	}
}

/**
* This class contains the core of the calendar module; defining properties and functions so the module fits into the IWP framework and dispatching requests for more specific stuff off to handler classes
*
* @package IWP_Modules
* @subpackage Calendar
*/
class iwp_module_calendar extends iwp_module {
	const TIME_FORMAT = 'h:i a';
	const JSON_DATETIME_FORMAT = 'YmdHi';

	const RRULE_STANDARD_DAILY = 'FREQ=DAILY';
	const RRULE_STANDARD_WEEKLY = 'FREQ=WEEKLY';
	const RRULE_STANDARD_MONTHLY = 'FREQ=MONTHLY';
	const RRULE_STANDARD_YEARLY = 'FREQ=YEARLY';

	/**
	 * Defines that this module has draggable blocks
	 *
	 * @var bool
	 */
	public $hasBlocks = true;

	/**
	 * A list of event listeners used for registering and unregistering events during the module's activation and deactivation.
	 *
	 * @var array
	 */
	protected $eventListeners = array(
		'iwp_event_admin_navigation_dropdownmenucreated' => array(
			'iwp_module_calendar', 'onDropdownMenuCreated'
		),
		'iwp_event_content_afterdbload' => array(
			'iwp_module_calendar', 'onContentAfterDbLoad'
		),
		'iwp_event_admin_content_tinymceplugin' => array(
			'iwp_module_calendar', 'onContentTinyMCEPlugin'
		),
		'iwp_event_lists_dynamicoutput' => array(
			'iwp_module_calendar', 'onListsDynamicOutput'
		),
		'iwp_event_categories_beforeshowtemplate' => array(
			'iwp_module_calendar', 'onCategoriesBeforeShowTemplate'
		),
		'iwp_event_template_outputblockbeforeparsesection' => array(
			'iwp_module_calendar', 'onTemplateOutputBlockBeforeParseSection'
		),
		'iwp_event_users_beforeprofiledisplay' => array(
			'iwp_module_calendar', 'onUserProfileDisplay'
		),
	);

	/**
	 *
	 * @var iwp_module_calendar
	 */
	public static $Instance;

	/**
	 *
	 * @var string
	 */
	public $moduleName = 'calendar';

	/**
	 *
	 * @var string
	 */
	protected $contentIdColumn = 'id';

	/**
	 *
	 * @var array
	 */
	protected $_columns = array(
		'id'						=> '',
		'contentdisplayposiiton'	=> '',
	);

	/**
	 *
	 * @return iwp_module_calendar
	 */
	public static function getInstance () {
		if(!isset(self::$Instance)){
			self::$Instance = new self();
		}
		return self::$Instance;
	}

	/**
	 * Returns a list of database tables for this module.
	 *
	 * @param boolean $withoutPrefix A boolean for whether or not to include the prefix for the table.
	 *
	 * @return string
	 */
	public function getTables($withoutPrefix=false) {
		$tables = array(
			'calendar',
			// 'calendar_exception', See the CreateModuleTable() function
			'calendar_event',
			'calendar_occurrencecache',
		);

		if($withoutPrefix) {
			return $tables;
		}

		foreach($tables as $k=>$table ){
			$tables[$k] = IWP_MODULE_DB_PREFIX . $table;
		}

		return $tables;
	}

	/**
	* Listener for the iwp_event_admin_content_tinymceplugin event. This adds a tinymce plugin for the calendar module.
	*
	* @param iwp_event_admin_content_tinymceplugin $data
	*/
	public static function onContentTinyMCEPlugin (iwp_event_admin_content_tinymceplugin $data) {
		$module = iwp_module_calendar::getInstance();

		//
		//	to reuse the code for the block modal dialog we need to present it with 'blockdata' variables

		$blockdata = array(
			'views'	=> array('day', 'week', 'month'),
			'first'	=> 'month',
			'title'	=> '',
		);

		$module->template->Assign('blockdata', $blockdata, false);

		//
		//	create language vars that this module should be using

		$vars = array();
		$vars['lang'] = $module->lang->GetLangVars();
		$module->template->Assign($module->moduleName, $vars, false);

		$vars['modalHTML'] = $module->template->ParseTemplate('block.displayoptions', true, $module->moduleName);
		$module->template->Assign($module->moduleName, $vars, false);	//	need to assign this twice because the modalHTML template needed the language vars above to work properly

		$module->template->AddRequiredCSS(IWP_MODULES_URI . '/' . $module->moduleName . '/templates/admin.calendar.css');
		$module->template->AddRequiredJS(IWP_MODULES_URI . '/' . $module->moduleName . '/javascript/jquery.htmlEncode.js');
		$module->template->AddRequiredJS(IWP_BASE_URI . '/javascript/jquery/plugins/json.js');

		//
		//	append the resulting js code to the tinymce loader code

		$data->code .= $module->template->ParseTemplate('calendar.tinymce.plugin', true, $module->moduleName);
	}

	/**
	* Processes calendar placeholders in the given string, replacing them with calendar module html
	*
	* @param string $string String to process (usually HTML)
	* @return string
	*/
	public function processContentPlaceholders ($string) {
		//	look for <p> tags immediately around the comment since this is a common occurrence with how tinymce works, but putting a div inside a p tag is bad xhtml
		$result = preg_match_all('#(?:<p>)?\s*<!--\s+iwp_module_calendar:(.*?)\s+-->\s*(?:</p>)?#ism', $string, $matches, PREG_SET_ORDER);

		if ($result === false || $result === 0) {
			//	preg error or no results
			return $string;
		}

		foreach ($matches as $match) {
			$json = json_decode(html_entity_decode_utf8($match[1]));

			$replace = $this->generateFrontendOutput('middle', @$json->views, @$json->first, @$json->title);

			$string = str_replace($match[0], $replace, $string);
		}

		return $string;
	}

	/**
	* Listener for the iwp_event_lists_dynamicoutput event which scans list output for calendar placeholders and replaces them with calendar output
	*
	* @param iwp_event_lists_dynamicoutput $data
	*/
	public static function onListsDynamicOutput (iwp_event_lists_dynamicoutput $data) {
		if (!isset($data->listRowArray['Summary']) || !$data->listRowArray['Summary']) {
			return;
		}

		$data->listRowArray['Summary'] = self::getInstance()->processContentPlaceholders($data->listRowArray['Summary']);
	}

	/**
	* Listener for the iwp_event_categories_beforeshowtemplate event which adjusts template output to make calendars appear on front end category descriptions
	*
	* @param iwp_event_categories_beforeshowtemplate $data
	*/
	public static function onCategoriesBeforeShowTemplate (iwp_event_categories_beforeshowtemplate $data) {
		$data->template->Assign('categoryDescription', self::getInstance()->processContentPlaceholders($data->template->Get('categoryDescription')), false);
	}

	/**
	* Listener for the iwp_event_template_outputblockbeforeparsesection event which adjusts custom block output to make calendars appear on front end custom blocks
	*
	* @param iwp_event_categories_beforeshowtemplate $data
	*/
	public static function onTemplateOutputBlockBeforeParseSection (iwp_event_template_outputblockbeforeparsesection $data) {
		switch (strtolower($data->type)) {
			case 'customcontent':
				$data->template->Assign('blockContent', self::getInstance()->processContentPlaceholders($data->template->Get('blockContent')), false);
				break;
		}
	}

	/**
	* Listener for the iwp_event_users_beforeprofiledisplay event which adjusts user biography output to make calendars appear on front end custom blocks
	*
	* @param iwp_event_users_beforeprofiledisplay $data
	*/
	public static function onUserProfileDisplay (iwp_event_users_beforeprofiledisplay $data) {
		$data->user->Set('biography', self::getInstance()->processContentPlaceholders($data->user->Get('biography')));
	}

	/**
	* Listener for the iwp_event_content_afterdbload event which scans loaded content for calendar placeholders and replaces them with calendar output
	*
	* @param iwp_event_content_afterdbload $data
	*/
	public static function onContentAfterDbLoad (iwp_event_content_afterdbload $data) {
		$module = self::getInstance();

		$replaceFields = array('content', 'summary');

		foreach ($replaceFields as $replaceFieldName) {
			$html = $data->content->Get($replaceFieldName);
			if (!$html) {
				//	blank field
				continue;
			}

			$html = self::getInstance()->processContentPlaceholders($html);
			$data->content->Set($replaceFieldName, $html);
		}
	}

	/**
	 * Listener for the iwp_event_admin_navigation_dropdownmenucreated event
	 *
	 * @param iwp_event_admin_navigation_dropdownmenucreated $data
	 */
	public static function onDropdownMenuCreated (iwp_event_admin_navigation_dropdownmenucreated $data) {
		$module = self::getInstance();
		$auth = iwp_admin_auth::getInstance();

		/*
		//	disabled the permission check here because calendars are publicly viewable anyway
		//	will need to re-enable this if it ever becomes possible to set events as not visible, because the admin view will then differ from the front end view
		if (!$auth->HasPerm('sitemodules', 'calendar', '*')) {
			return;
		}
		*/

		$mnuContent = array();

		foreach($data->menuItems['mnuContent'] as $key => $menuItem) {
			$mnuContent[] = $menuItem;
			if(isset($menuItem['link']) && $menuItem['link'] == 'index.php?section=categories&amp;action=view') {
				$mnuContent[] = array(
					'text' => $module->lang->Get('menuManageCalendar'),
					'link' => 'index.php?section=module&action=custom&module=calendar',
					'show' => true,
					'help' => $module->lang->Get('menuManageCalendarHelp'),
					'icon' => '../../modules/calendar/images/calendar.png',
				);
			}
		}

		$data->menuItems['mnuContent'] = $mnuContent;

		if (@$_GET['section'] == 'module' && @$_GET['action'] == 'custom' && @$_GET['module'] == 'calendar') {
			$data->currentMenu = 'mnuCalendar';
		}
	}

	/**
	* ArrayInsert
	* Inserts one array into another at a given position.
	*
	* @param array $array The recipient array to be inserted into, passed by reference and will have it's pointer reset
	* @param mixed $position The index (numeric) position at which to insert
	* @param mixed $insert_array The array to be inserted
	*/
	private static function ArrayInsert (&$array, $position, $insert_array) {
		$first_array = array_splice ($array, 0, $position);
		$array = array_merge ($first_array, $insert_array, $array);
	}

	/**
	 * (non-PHPdoc)
	 * @see api/iwp_module#IsContentModule()
	 */
	public function IsContentModule () { return false; }

	/**
	 * (non-PHPdoc)
	 * @see api/iwp_module#IsWebsiteModule()
	 */
	public function IsWebsiteModule () { return true; }

	/**
	 * (non-PHPdoc)
	 * @see api/iwp_module#HasConfigurationScreen()
	 */
	public function HasConfigurationScreen () {
		return false;
	}

	/**
	 * (non-PHPdoc)
	 * @see api/iwp_module#CreateModuleTable()
	 */
	public function CreateModuleTable () {
		$this->table = array();
		$this->tableSchema = array();

		//	settings table
		$table = new iwp_tablecreator($this->getTable('calendar'));
		$table->AddField('id')->SetAsPrimaryKey();
		$table->AddField('contentdisplayposition')->SetAsVarChar('255');
		$this->table[] = $table;
		$this->tableSchema[$table->name] = $table->GetCreateTable();

		//	events table
		$table = new iwp_tablecreator($this->getTable('calendar_event'));
		$table->AddField('id')->SetAsPrimaryKey();
		$table->AddField('description')->SetAsText()->SetIsNull(true)->SetDefault(null);
		$table->AddField('dtstart')->SetAsChar(12)->SetIsNull(true)->SetDefault(null);
		$table->AddField('dtstart_tz')->SetAsVarChar(255)->SetIsNull(true)->SetDefault(null);
		$table->AddField('dtend')->SetAsChar(12)->SetIsNull(true)->SetDefault(null);
		$table->AddField('dtend_tz')->SetAsVarChar(255)->SetIsNull(true)->SetDefault(null);
		$table->AddField('location')->SetAsText()->SetIsNull(true)->SetDefault(null);
		$table->AddField('sequence')->SetAsInt()->SetIsNull(true)->SetDefault(0);
		$table->AddField('uid')->SetAsVarChar(255)->SetIsNull(true)->SetDefault(null);
		$table->AddField('created')->SetAsDateTime()->SetIsNull(true)->SetDefault(null);
		$table->AddField('summary')->SetAsText()->SetIsNull(true)->SetDefault(null);
		$table->AddField('class')->SetAsVarChar(255)->SetIsNull(true)->SetDefault(null);
		$table->AddField('rrule')->SetAsText()->SetIsNull(true)->SetDefault(null);
		$table->AddField('color')->SetAsVarChar(6)->SetIsNull(true)->SetDefault(null);
		$this->table[] = $table;
		$this->tableSchema[$table->name] = $table->GetCreateTable();

		//	occurrence cache table
		$table = new iwp_tablecreator($this->getTable('calendar_occurrencecache'));
		$table->AddField('cacheblock')->SetAsChar(6)->SetAsPrimaryKey(false);
		$table->AddField('tz')->SetAsVarChar(50)->SetAsPrimaryKey(false);
		$table->AddField('data')->SetAsText();
		$this->table[] = $table;
		$this->tableSchema[$table->name] = $table->GetCreateTable();

		/*
		// exceptions table
		// if this is ever uncommented, you must uncomment the line in the getTables() function that hides this table
		$table = new iwp_tablecreator($this->getTable('calendar_exception'));
		$table->AddField('event')->SetAsPrimaryKey(false)->SetAsInt();
		$table->AddField('exdate')->SetAsPrimaryKey(false)->Set('type', 'timestamp')->SetDefault('0000-00-00 00:00:00');
		$this->table[] = $table;
		$this->tableSchema[$table->name] = $table->GetCreateTable();
		*/
	}

	/**
	* Custom activation routine for the calendar module
	*
	* @return boolean
	*/
	public function ActivateModule () {
		if (version_compare(PHP_VERSION, '5.2.0') === -1) {
			$this->messages[] = $this->lang->Get('RequiresPHP52');
			return false;
		}

		return parent::ActivateModule();
	}

	/**
	 * Passes remote actions through to handler classes
	 *
	 * @return unknown_type
	 */
	public function RemoteAction () {
		$moduleAction = preg_replace('#[^a-zA-Z0-9]#', '', iwp_strtolower(@$_GET['moduleaction']));
		if (!$moduleAction) {
			$moduleAction = 'view';
		}

		$className = 'iwp_module_calendar_adminremote' . $moduleAction . 'action';
		if (!class_exists($className)) {
			throw new Exception('RemoteAction class name not found for: ' . $moduleAction);
		}

		$actionHandler = new $className($this, false, false);
		$actionHandler->handleAction();
		die();
	}

	/**
	 * (non-PHPdoc)
	 * @see api/iwp_module#AdminCustom()
	 */
	public function AdminCustom () {
		$moduleAction = preg_replace('#[^a-zA-Z0-9]#', '', iwp_strtolower(@$_GET['moduleaction']));
		if (!$moduleAction) {
			$moduleAction = 'view';
		}

		$className = 'iwp_module_calendar_admin' . $moduleAction . 'action';
		if (!class_exists($className)) {
			$className = 'iwp_module_calendar_adminviewaction';
		}

		$actionHandler = new $className($this);
		$actionHandler->handleAction();
		die();
	}

	/**
	 * (non-PHPdoc)
	 * @see api/iwp_module#ShowPageByIniUri()
	 */
	public function ShowPageByIniUri () {
		$matchedUri = $this->urls->GetCurrentModulePage();
		$uriBits	= $this->urls->GetCurrentMatches();

		if ($matchedUri == 'Remote') {
			$data = explode('/', $uriBits['structure']);
			$action = array_shift($data);
			$extension = $uriBits['extension'];

			$className = 'iwp_module_calendar_remote' . $action . 'action';
			if (!class_exists($className)) {
				$this->controller->ShowPage('NotFound');
				die();
			}

			$handler = new $className($this, $data, $extension);
			$handler->handleAction();
		}
	}

	/**
	* Storage for this module's site-wide permission options
	*
	* @var array
	*/
	public static $SitePermissionOptions;

	/**
	 * Returns this module's site-wide permission options
	 *
	 * @return array
	 */
	public static function GetSitePermissionOptions () {
		return iwp_module_calendar::$SitePermissionOptions;
	}

	public function addAdminResources () {
		$this->addAdminJavascript();
		$this->addAdminCSS();
	}

	/**
	 * Adds common js files used by the calendar admin pages to the template output
	 *
	 * @return void Returns nothing
	 */
	public function addAdminJavascript () {
		$this->template->AddRequiredJS(iwp_config::Get('siteURL') . '/javascript/jquery.form.js');
		$this->template->AddRequiredJS(iwp_config::Get('siteURL') . '/javascript/jquery.field.js');
		$this->template->AddRequiredJS(iwp_config::Get('siteURL') . '/js.php?f=admin.modules&section=module&a=1');
		$this->template->AddRequiredJS(iwp_config::Get('siteURL') . '/js.php?f=admin.validator&section=module&a=1');
		$this->template->AddRequiredJS(iwp_config::Get('siteURL') . '/javascript/jquery/plugins/jquery.interspireAjaxForm.js');
		$this->template->AddRequiredJS(iwp_config::Get('siteURL') . '/javascript/date.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/colorpicker/js/colorpicker.js');

		//	if minify js is enabled, use this
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.js');

		//	during development, use this
		/*
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/jquery.htmlEncode.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/jquery.revealer.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/jquery.calendarBubble.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/jquery.dateRange.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.common.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.views.week.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.views.day.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.views.month.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.views.tinymonth.js');
		*/

		//	admin-specific js
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/admin.' . $this->moduleName . '.js');
	}

	/**
	 * Adds common css files used by the calendar admin pages to the template output
	 *
	 * @return void Returns nothing
	 */
	public function addAdminCSS () {
		$this->template->AddRequiredCSS(IWP_MODULES_URI . '/' . $this->moduleName . '/colorpicker/css/colorpicker.css');
		$this->template->AddRequiredCSS(IWP_MODULES_URI . '/' . $this->moduleName . '/colorpicker/css/colorpicker.local.css');
		$this->template->AddRequiredCSS(iwp_config::Get('siteURL') . '/css.php?m=' . urlencode($this->moduleName) . '&f=styles');
		$this->template->AddRequiredCSS(IWP_MODULES_URI . '/' . $this->moduleName . '/templates/admin.' . $this->moduleName . '.css');
	}

	public function displaySessionTemplateMessages () {
		if ($this->session->Exists('successMessage')) {
			$this->template->Assign('MessageText', $this->session->Get('successMessage'));
			$this->template->Assign('MessageType', MSG_SUCCESS);
		}
	}

	/**
	 * Taken from undocumented function in twitter module
	 *
	 * @see api/iwp_module#GetLayoutBlocks()
	 */
	public function GetLayoutBlocks(){
		$icon =  IWP_MODULES_URI . '/' . $this->moduleName . '/images/calendar.png';

		$blocks = array();

		$blocks['CalendarDisplay'] = array(
			'id'		=> 'CalendarDisplay',
			'name'		=> $this->lang->Get('CalendarDisplayBlockName'),
			'icon'		=> $icon,
			'hasPopup'	=> true,
		);

		$blocks['CompactDisplay'] = array(
			'id'		=> 'CompactDisplay',
			'name'		=> $this->lang->Get('CompactDisplayBlockName'),
			'icon'		=> $icon,
			'hasPopup'	=> false,
		);

		$this->template->AddLanguageForJS(array(
			'CalendarDisplayBlockName',
			'SelectFirstError',
			'SelectViewsError',
		), $this->lang, 'module_calendar_');

		return $blocks;
	}

	/**
	* Returns HTML for displaying on the front end and adds the required css and js includes to the current template instance
	*
	* @param string $position
	* @param array $views
	* @param string $first
	* @param string $title
	*/
	public function generateFrontendOutput ($position, $views = null, $first = null, $title = null) {
		if ($views === null || count($views) < 1) {
			$views = array(
				'day',
				'week',
				'month',
			);
		}

		if ($first === null || count($views) == 1) {
			$first = $views[0];
		}

		if ($title === null) {
			$title = '';
		}

		//$this->template->AddRequiredCSS(IWP_MODULES_URI . '/' . $this->moduleName . '/templates/styles.css');

		//	when minified code is enabled, use this
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.js');

		//	during development, use this
		/*
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/jquery.htmlEncode.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/jquery.revealer.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/jquery.calendarBubble.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/jquery.dateRange.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.common.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.views.week.js');	//	the day view depends on having week view js available so always include it, and include it before
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.views.day.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.views.month.js');
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.views.tinymonth.js');
		*/

		//	frontend specific js
		$this->template->AddRequiredJS(IWP_MODULES_URI . '/' . $this->moduleName . '/javascript/' . $this->moduleName . '.frontend.js');

		//	frontend language js

		$this->template->AddLanguageForJS(array(
			'Starts',
			'Ends',
			'Location',
			'more',
			'event',
			'Thereareeventsscheduledtoday',
			'Therearenoevents',
			'Today',
			'ViewName_Day',
			'ViewName_Week',
			'ViewName_Month',
			'ViewName_Tinymonth',
			'ClosePopup',
			'alldayevent',
		), $this->lang, 'module_calendar_');

		$data = array();
		$data['lang'] = $this->lang->GetLangVars();
		$this->template->Assign($this->moduleName, $data, false);

		$instanceId = iwp_module_calendar_event::generateUid();

		$viewsHTML = '';

		foreach ($views as $viewName) {
			$firstViewClass = '';
			if ($first == $viewName) {
				$firstViewClass = 'calendar-firstview';
			}

			$viewsHTML .= '<div class="calendar-view calendar-view-' . $viewName . ' ' . $firstViewClass . '" style="display:none;">';

			//	if a template exists for this view, parse it, otherwise leave it as just the containing div
			if (file_exists(IWP_MODULES_PATH . '/' . $this->moduleName . '/templates/calendar.views.' . $viewName . '.html')) {
				$viewsHTML .=  $this->template->ParseTemplate('calendar.views.' . strtolower($viewName), true, $this->moduleName);
			}

			$viewsHTML .= '</div>';
		}

		$calendarHTML = '<div class="calendar-container calendar-container-frontend">' . $viewsHTML . '</div>';

		if ($title) {
			$this->template->Assign('blockTitle', $title);
			$this->template->Assign('blockContent', $calendarHTML, false);

			$this->template->Assign(array('calendar', 'american'), iwp_settings::IsAmericanDateFormat(iwp_getShortDateFormat()), false);

			return $this->template->ParseSection('customcontent_' . $position . '_standard');
		}

		return $calendarHTML;
	}

	/**
	* Outputs a block for this module which is saved in the template_data table
	*
	* @param int $blockId The block id to output
	* @param string $position
	* @return string The resulting output HTML
	*/
	public function OutputSavedBlock ($blockId, $position) {
		$blockId = (int)$blockId;

		$result = $this->db->Query("SELECT `type`, `content` FROM `" . IWP_TABLE_TEMPLATE_DATA . "` WHERE `blockid` = " . $blockId);

		$row = $this->db->Fetch($result);

		if (!$row) {
			return '';
		}

		$data = unserialize($row['content']);

		return $this->generateFrontendOutput($position, @$data['views'], @$data['first'], @$data['title']);
	}

	/**
	 * Taken from undocumented function in twitter module
	 *
	 * @return string
	 */
	public function OutputBlock ($blockName, $position, $data = null) {
		//	verify that the requested block is provided by this module
		$blocks = $this->GetLayoutBlocks();
		if (!isset($blocks[$blockName])) {
			return '';
		}

		return $this->generateFrontendOutput($position, array('tinymonth'), 'tinymonth', '');
	}

	/**
	* Implements conversion of request data into data insertable to the template_data table for this module
	*
	* @param array $request
	* @param array $insert
	* @return boolean
	*/
	public function getSavedBlockInsert ($request, &$insert) {
		$insert['name'] = $this->lang->Get('CalendarDisplayBlockName');

		$data = array(
			'views'		=> $request['views'],
			'first'		=> $request['first'],
			'title'		=> $request['title'],
		);

		$insert['content'] = serialize($data);

		// This associates the saved block to the layoutBlocks array in this module,
		// it then allows the right icon to be added to the block
		$insert['title'] = 'CalendarDisplay';

		return true;
	}

	/**
	* Implements output of an edit saved block template for this module. See parent function for more info.
	*
	* @param int $blockId
	* @param array $data
	* @return mixed
	*/
	public function getEditSavedBlockTemplate ($blockId, $data) {
		$parameters = array(
			'blockId'	=> $blockId,
			'data'		=> $data,
		);

		$action = new iwp_module_calendar_adminremotecalendardisplayoptionsaction($this, $parameters, false);

		ob_start();
		$action->handleAction();
		$output = ob_get_contents();
		ob_end_clean();
		return $output;
	}

	/**
	 * Flushes the occurrence cache
	 *
	 * @return bool
	 */
	public static function flushOccurrenceCache () {
		return self::getInstance()->db->Query("TRUNCATE " . GetConfig('tablePrefix') . "module_calendar_occurrencecache");
	}

	/**
	 * Returns a granularity list for this class.
	 *
	 * @param Integer $total Total will be populated with number of rows found in query (by reference)
	 * @param String $filter Filter string, optional
	 * @param Integer $page Page number of records to return, optional
	 * @return Array List of value/text pairs
	 */
	public static function getGranularityList (&$total, &$page, $filter = '') {
		$module = iwp_module_calendar::getInstance();
		$event = iwp_module_calendar_event::getInstance();

		$limitStart = ($page * IWP_PERMISSIONGRANULARITEMS_PER_PAGE) - IWP_PERMISSIONGRANULARITEMS_PER_PAGE;
		$where = '';
		if ($filter) {
			$filter = '%'. self::getInstance()->db->Quote($filter) .'%';
			$where = sprintf("WHERE (`summary` LIKE '%s')", $filter);
		}

		$result = self::getInstance()->db->Query(sprintf("SELECT SQL_CALC_FOUND_ROWS `id` AS `value`, `summary` AS `text` FROM %s %s ORDER BY `summary` LIMIT %d, %d", $module->getTable('calendar_event'), $where, $limitStart, IWP_PERMISSIONGRANULARITEMS_PER_PAGE));
		$total = self::getInstance()->db->FetchOne('SELECT found_rows()');
		$list = array();
		if ($result) {
			while ($row = self::getInstance()->db->Fetch($result)) {
				$row['value'] = (int)$row['value'];
				array_push($list, $row);
			}
			self::getInstance()->db->FreeResult($result);
		}
		return $list;
	}
}

/**
 * @see iwp_module::$SitePermissionOptions
 */
iwp_module_calendar::$SitePermissionOptions = array(
	'full'			=> new iwp_permissionoption(true, false, false),
	'eventcreate'	=> new iwp_permissionoption(false, false, false),
	'eventedit'		=> new iwp_permissionoption(true, false, false),
	'eventdelete'	=> new iwp_permissionoption(true, false, false),
);