calendarimporter/php/module.calendar.php

618 lines
18 KiB
PHP
Raw Normal View History

2016-06-20 16:19:05 +02:00
<?php
/**
* module.calendar.php, zarafa calender to ics im/exporter
*
* Author: Christoph Haas <christoph.h@sprinternet.at>
* Copyright (C) 2012-2014 Christoph Haas
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
2016-06-20 15:04:53 +02:00
2016-06-20 16:19:05 +02:00
include_once('vendor/autoload.php');
2016-06-20 19:42:07 +02:00
use Sabre\VObject;
2016-06-20 16:19:05 +02:00
class CalendarModule extends Module
{
private $DEBUG = true; // enable error_log debugging
private $busystates = null;
private $labels = null;
private $attendeetype = null;
/**
* @constructor
* @param $id
* @param $data
*/
2016-06-20 16:19:05 +02:00
public function __construct($id, $data)
{
parent::Module($id, $data);
// init default timezone
date_default_timezone_set(PLUGIN_CALENDARIMPORTER_DEFAULT_TIMEZONE);
// init mappings
$this->busystates = array(
"FREE",
"TENTATIVE",
"BUSY",
"OOF"
);
$this->labels = array(
"NONE",
"IMPORTANT",
"WORK",
"PERSONAL",
"HOLIDAY",
"REQUIRED",
"TRAVEL REQUIRED",
"PREPARATION REQUIERED",
"BIRTHDAY",
"SPECIAL DATE",
"PHONE INTERVIEW"
);
$this->attendeetype = array(
"NON-PARTICIPANT", // needed as zarafa starts counting at 1
"REQ-PARTICIPANT",
"OPT-PARTICIPANT",
"NON-PARTICIPANT"
);
}
/**
* Executes all the actions in the $data variable.
* Exception part is used for authentication errors also
* @return boolean true on success or false on failure.
*/
2016-06-20 16:19:05 +02:00
public function execute()
{
$result = false;
2016-06-20 16:19:05 +02:00
if (!$this->DEBUG) {
2013-03-30 14:55:18 +01:00
/* disable error printing - otherwise json communication might break... */
ini_set('display_errors', '0');
}
2016-06-20 16:19:05 +02:00
foreach ($this->data as $actionType => $actionData) {
if (isset($actionType)) {
try {
2016-06-20 16:19:05 +02:00
if ($this->DEBUG) {
error_log("exec: " . $actionType);
}
2016-06-20 16:19:05 +02:00
switch ($actionType) {
case "load":
$result = $this->loadCalendar($actionType, $actionData);
break;
case "export":
$result = $this->exportCalendar($actionType, $actionData);
2013-03-30 14:55:18 +01:00
break;
case "import":
$result = $this->importCalendar($actionType, $actionData);
break;
2016-06-20 15:48:24 +02:00
case "importattachment":
2013-03-30 14:55:18 +01:00
$result = $this->getAttachmentPath($actionType, $actionData);
break;
default:
$this->handleUnknownActionType($actionType);
}
} catch (MAPIException $e) {
2016-06-20 16:19:05 +02:00
if ($this->DEBUG) {
error_log("mapi exception: " . $e->getMessage());
}
} catch (Exception $e) {
2016-06-20 16:19:05 +02:00
if ($this->DEBUG) {
error_log("exception: " . $e->getMessage());
}
}
}
}
2016-06-20 16:19:05 +02:00
return $result;
}
2016-06-20 16:19:05 +02:00
2013-03-30 14:55:18 +01:00
/**
* Generates a random string with variable length.
* @param $length the lenght of the generated string
* @return string a random string
*/
2016-06-20 16:19:05 +02:00
private function randomstring($length = 6)
{
// $chars - all allowed charakters
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
2016-06-20 16:19:05 +02:00
srand((double)microtime() * 1000000);
$i = 0;
2013-03-30 14:55:18 +01:00
$pass = "";
while ($i < $length) {
$num = rand() % strlen($chars);
$tmp = substr($chars, $num, 1);
$pass = $pass . $tmp;
$i++;
}
return $pass;
}
2016-06-20 16:19:05 +02:00
/**
* Get a property from the array.
* @param $props
* @param $propname
* @return string
*/
private function getProp($props, $propname)
{
if (isset($props["props"][$propname])) {
return $props["props"][$propname];
}
return "";
}
private function getDurationStringFromMintues($minutes, $pos = false) {
$pos = $pos === true ? "+" : "-";
$str = $pos . "P";
// variables for holding values
$mins = intval($minutes);
$hours = 0;
$days = 0;
$weeks = 0;
// calculations
if ( $mins >= 60 ) {
$hours = (int)($mins / 60);
$mins = $mins % 60;
}
if ( $hours >= 24 ) {
$days = (int)($hours / 24);
$hours = $hours % 60;
}
if ( $days >= 7 ) {
$weeks = (int)($days / 7);
$days = $days % 7;
}
// format result
if ( $weeks ) {
$str .= "{$weeks}W";
}
if ( $days ) {
$str .= "{$days}D";
}
if ( $hours ) {
$str .= "{$hours}H";
}
if ( $mins ) {
$str .= "{$mins}M";
}
return $str;
}
2013-03-30 14:55:18 +01:00
/**
* The main export function, creates the ics file for download
* @param $actionType
* @param $actionData
*/
2016-06-20 16:19:05 +02:00
private function exportCalendar($actionType, $actionData)
{
// Get store id
$storeid = false;
if (isset($actionData["storeid"])) {
$storeid = $actionData["storeid"];
}
// Get records
$records = array();
if (isset($actionData["records"])) {
$records = $actionData["records"];
}
// Get folders
$folder = false;
if (isset($actionData["folder"])) {
$folder = $actionData["folder"];
}
$response = array();
$error = false;
$error_msg = "";
// write csv
$token = $this->randomstring(16);
$file = PLUGIN_CALENDARIMPORTER_TMP_UPLOAD . "ics_" . $token . ".ics";
file_put_contents($file, "");
$store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeid));
if ($store) {
// load folder first
if ($folder !== false) {
$mapifolder = mapi_msgstore_openentry($store, hex2bin($folder));
$table = mapi_folder_getcontentstable($mapifolder);
$list = mapi_table_queryallrows($table, array(PR_ENTRYID));
foreach ($list as $item) {
$records[] = bin2hex($item[PR_ENTRYID]);
}
}
$vcalendar = new VObject\Component\VCalendar();
for ($index = 0, $count = count($records); $index < $count; $index++) {
$message = mapi_msgstore_openentry($store, hex2bin($records[$index]));
// get message properties.
$properties = $GLOBALS['properties']->getAppointmentProperties();
$plaintext = true;
$messageProps = $GLOBALS['operations']->getMessageProps($store, $message, $properties, $plaintext);
$vevent = $vcalendar->add('VEVENT', [
'SUMMARY' => $this->getProp($messageProps, "subject"),
'DTSTART' => date_timestamp_set(new DateTime(), $this->getProp($messageProps, "startdate")),
'DTEND' => date_timestamp_set(new DateTime(), $this->getProp($messageProps, "duedate")),
'CREATED' => date_timestamp_set(new DateTime(), $this->getProp($messageProps, "creation_time")),
'LAST-MODIFIED' => date_timestamp_set(new DateTime(), $this->getProp($messageProps, "last_modification_time")),
'PRIORITY' => $this->getProp($messageProps, "importance"),
'X-MICROSOFT-CDO-INTENDEDSTATUS' => $this->busystates[intval($this->getProp($messageProps, "busystatus"))], // both seem to be valid...
'X-MICROSOFT-CDO-BUSYSTATUS' => $this->busystates[intval($this->getProp($messageProps, "busystatus"))], // both seem to be valid...
'X-ZARAFA-LABEL' => $this->labels[intval($this->getProp($messageProps, "label"))],
'CLASS' => $this->getProp($messageProps, "private") ? "PRIVATE" : "PUBLIC",
'COMMENT' => "eid:" . $records[$index]
]);
// Add organizer
$vevent->add('ORGANIZER','mailto:' . $this->getProp($messageProps, "sender_email_address"));
$vevent->ORGANIZER['CN'] = $this->getProp($messageProps, "sender_name");
// Add Attendees
if(isset($messageProps["recipients"]) && count($messageProps["recipients"]["item"]) > 0) {
foreach($messageProps["recipients"]["item"] as $attendee) {
$att = $vevent->add('ATTENDEE', "mailto:" . $this->getProp($attendee, "email_address"));
$att["CN"] = $this->getProp($attendee, "display_name");
$att["ROLE"] = $this->attendeetype[intval($this->getProp($attendee, "recipient_type"))];
}
}
// Add alarms
if(!empty($this->getProp($messageProps, "reminder")) && $this->getProp($messageProps, "reminder") == 1) {
$valarm = $vevent->add('VALARM', [
'ACTION' => 'DISPLAY',
'DESCRIPTION' => $this->getProp($messageProps, "subject") // reuse the event summary
]);
// Add trigger
$durationValue = $this->getDurationStringFromMintues($this->getProp($messageProps, "reminder_minutes"), false);
$valarm->add('TRIGGER', $durationValue); // default trigger type is duration (see 4.8.6.3)
/*
$valarm->add('TRIGGER', date_timestamp_set(new DateTime(), $this->getProp($messageProps, "reminder_time"))); // trigger type "DATE-TIME"
$valarm->TRIGGER['VALUE'] = 'DATE-TIME';
*/
}
// Add location
if(!empty($this->getProp($messageProps, "location"))) {
$vevent->add('LOCATION',$this->getProp($messageProps, "location"));
}
// Add description
$body = $this->getProp($messageProps, "isHTML") ? $this->getProp($messageProps, "html_body") : $this->getProp($messageProps, "body");
if(!empty($body)) {
$vevent->add('DESCRIPTION',$body);
}
}
// write combined ics file
file_put_contents($file, file_get_contents($file) . $vcalendar->serialize());
}
if (count($records) > 0) {
$response['status'] = true;
$response['download_token'] = $token;
$response['filename'] = count($records) . "events.ics";
} else {
$response['status'] = false;
$response['message'] = "No events found. Export skipped!";
}
$this->addActionData($actionType, $response);
$GLOBALS["bus"]->addData($this->getResponseData());
2013-03-30 14:55:18 +01:00
}
2016-06-20 16:19:05 +02:00
2013-03-30 14:55:18 +01:00
/**
* The main import function, parses the uploaded ics file
* @param $actionType
* @param $actionData
*/
2016-06-20 16:19:05 +02:00
private function importCalendar($actionType, $actionData)
{
// TODO: implement
2013-03-30 14:55:18 +01:00
}
2016-06-20 16:19:05 +02:00
2013-03-30 14:55:18 +01:00
/**
* Store the file to a temporary directory, prepare it for oc upload
* @param $actionType
* @param $actionData
* @private
*/
2016-06-20 16:19:05 +02:00
private function getAttachmentPath($actionType, $actionData)
{
2013-03-30 14:55:18 +01:00
// Get store id
$storeid = false;
2016-06-20 16:19:05 +02:00
if (isset($actionData["store"])) {
2013-03-30 14:55:18 +01:00
$storeid = $actionData["store"];
}
// Get message entryid
$entryid = false;
2016-06-20 16:19:05 +02:00
if (isset($actionData["entryid"])) {
2013-03-30 14:55:18 +01:00
$entryid = $actionData["entryid"];
}
// Check which type isset
$openType = "attachment";
// Get number of attachment which should be opened.
$attachNum = false;
2016-06-20 16:19:05 +02:00
if (isset($actionData["attachNum"])) {
2013-03-30 14:55:18 +01:00
$attachNum = $actionData["attachNum"];
}
// Check if storeid and entryid isset
2016-06-20 16:19:05 +02:00
if ($storeid && $entryid) {
2013-03-30 14:55:18 +01:00
// Open the store
$store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeid));
2016-06-20 16:19:05 +02:00
if ($store) {
2013-03-30 14:55:18 +01:00
// Open the message
$message = mapi_msgstore_openentry($store, hex2bin($entryid));
2016-06-20 16:19:05 +02:00
if ($message) {
2013-03-30 14:55:18 +01:00
$attachment = false;
// Check if attachNum isset
2016-06-20 16:19:05 +02:00
if ($attachNum) {
2013-03-30 14:55:18 +01:00
// Loop through the attachNums, message in message in message ...
2016-06-20 16:19:05 +02:00
for ($i = 0; $i < (count($attachNum) - 1); $i++) {
2013-03-30 14:55:18 +01:00
// Open the attachment
2016-06-20 16:19:05 +02:00
$tempattach = mapi_message_openattach($message, (int)$attachNum[$i]);
if ($tempattach) {
2013-03-30 14:55:18 +01:00
// Open the object in the attachment
$message = mapi_attach_openobj($tempattach);
}
}
// Open the attachment
2016-06-20 16:19:05 +02:00
$attachment = mapi_message_openattach($message, (int)$attachNum[(count($attachNum) - 1)]);
2013-03-30 14:55:18 +01:00
}
// Check if the attachment is opened
2016-06-20 16:19:05 +02:00
if ($attachment) {
2013-03-30 14:55:18 +01:00
// Get the props of the attachment
$props = mapi_attach_getprops($attachment, array(PR_ATTACH_LONG_FILENAME, PR_ATTACH_MIME_TAG, PR_DISPLAY_NAME, PR_ATTACH_METHOD));
// Content Type
$contentType = "application/octet-stream";
// Filename
$filename = "ERROR";
// Set filename
2016-06-20 16:19:05 +02:00
if (isset($props[PR_ATTACH_LONG_FILENAME])) {
2013-03-30 14:55:18 +01:00
$filename = $props[PR_ATTACH_LONG_FILENAME];
2016-06-20 16:19:05 +02:00
} else {
if (isset($props[PR_ATTACH_FILENAME])) {
$filename = $props[PR_ATTACH_FILENAME];
} else {
if (isset($props[PR_DISPLAY_NAME])) {
$filename = $props[PR_DISPLAY_NAME];
}
}
}
2013-03-30 14:55:18 +01:00
// Set content type
2016-06-20 16:19:05 +02:00
if (isset($props[PR_ATTACH_MIME_TAG])) {
2013-03-30 14:55:18 +01:00
$contentType = $props[PR_ATTACH_MIME_TAG];
} else {
// Parse the extension of the filename to get the content type
2016-06-20 16:19:05 +02:00
if (strrpos($filename, ".") !== false) {
2013-03-30 14:55:18 +01:00
$extension = strtolower(substr($filename, strrpos($filename, ".")));
$contentType = "application/octet-stream";
2016-06-20 16:19:05 +02:00
if (is_readable("mimetypes.dat")) {
$fh = fopen("mimetypes.dat", "r");
2013-03-30 14:55:18 +01:00
$ext_found = false;
2016-06-20 16:19:05 +02:00
while (!feof($fh) && !$ext_found) {
2013-03-30 14:55:18 +01:00
$line = fgets($fh);
preg_match("/(\.[a-z0-9]+)[ \t]+([^ \t\n\r]*)/i", $line, $result);
2016-06-20 16:19:05 +02:00
if ($extension == $result[1]) {
2013-03-30 14:55:18 +01:00
$ext_found = true;
$contentType = $result[2];
}
}
fclose($fh);
}
}
}
2016-06-20 16:19:05 +02:00
2013-03-30 14:55:18 +01:00
$tmpname = tempnam(TMP_PATH, stripslashes($filename));
// Open a stream to get the attachment data
$stream = mapi_openpropertytostream($attachment, PR_ATTACH_DATA_BIN);
$stat = mapi_stream_stat($stream);
// File length = $stat["cb"]
2016-06-20 16:19:05 +02:00
$fhandle = fopen($tmpname, 'w');
2013-03-30 14:55:18 +01:00
$buffer = null;
2016-06-20 16:19:05 +02:00
for ($i = 0; $i < $stat["cb"]; $i += BLOCK_SIZE) {
2013-03-30 14:55:18 +01:00
// Write stream
$buffer = mapi_stream_read($stream, BLOCK_SIZE);
2016-06-20 16:19:05 +02:00
fwrite($fhandle, $buffer, strlen($buffer));
2013-03-30 14:55:18 +01:00
}
fclose($fhandle);
2016-06-20 16:19:05 +02:00
2013-03-30 14:55:18 +01:00
$response = array();
$response['tmpname'] = $tmpname;
$response['filename'] = $filename;
$response['status'] = true;
$this->addActionData($actionType, $response);
$GLOBALS["bus"]->addData($this->getResponseData());
}
}
} else {
$response['status'] = false;
$response['message'] = "Store could not be opened!";
$this->addActionData($actionType, $response);
$GLOBALS["bus"]->addData($this->getResponseData());
}
} else {
$response['status'] = false;
$response['message'] = "Wrong call, store and entryid have to be set!";
$this->addActionData($actionType, $response);
$GLOBALS["bus"]->addData($this->getResponseData());
}
}
2016-06-20 16:19:05 +02:00
/**
* Function that parses the uploaded ics file and posts it via json
* @param $actionType
* @param $actionData
*/
private function loadCalendar($actionType, $actionData)
{
$error = false;
$error_msg = "";
if (is_readable($actionData["ics_filepath"])) {
$parser = null;
try {
2016-06-20 19:42:07 +02:00
$parser = VObject\Reader::read(
fopen($actionData["ics_filepath"],'r')
);
error_log(print_r($parser->VTIMEZONE, true));
2016-06-20 16:19:05 +02:00
} catch (Exception $e) {
$error = true;
$error_msg = $e->getMessage();
}
if ($error) {
$response['status'] = false;
$response['message'] = $error_msg;
} else {
2016-06-20 19:42:07 +02:00
if (count($parser->VEVENT) == 0) {
2016-06-20 16:19:05 +02:00
$response['status'] = false;
$response['message'] = "No event in ics file";
} else {
$response['status'] = true;
$response['parsed_file'] = $actionData["ics_filepath"];
$response['parsed'] = array(
2016-06-20 19:42:07 +02:00
'events' => $this->parseCalendarToArray($parser),
2016-06-20 20:00:44 +02:00
'timezone' => isset($parser->VTIMEZONE->TZID) ? (string)$parser->VTIMEZONE->TZID : (string)$parser->{'X-WR-TIMEZONE'},
'calendar' => (string)$parser->PRODID
2016-06-20 16:19:05 +02:00
);
}
}
} else {
$response['status'] = false;
$response['message'] = "File could not be read by server";
}
$this->addActionData($actionType, $response);
$GLOBALS["bus"]->addData($this->getResponseData());
if ($this->DEBUG) {
error_log("parsing done, bus data written!");
}
}
/**
* Create a array with contacts
*
2016-06-20 19:42:07 +02:00
* @param {VObject} $calendar ics parser object
2016-06-20 16:19:05 +02:00
* @return array parsed events
* @private
*/
private function parseCalendarToArray($calendar)
{
2016-06-20 19:42:07 +02:00
$events = array();
foreach ($calendar->VEVENT as $Index => $vEvent) {
// Sabre\VObject\Parser\XML\Element\VEvent
$properties = array();
//uid - used for front/backend communication
$properties["internal_fields"] = array();
$properties["internal_fields"]["event_uid"] = base64_encode($Index . $vEvent->UID);
$properties["startdate"] = (string)$vEvent->DTSTART->getDateTime()->getTimestamp();
$properties["enddate"] = (string)$vEvent->DTEND->getDateTime()->getTimestamp();
$properties["location"] = (string)$vEvent->LOCATION;
$properties["subject"] = (string)$vEvent->SUMMARY;
$properties["body"] = (string)$vEvent->DESCRIPTION;
$properties["comment"] = (string)$vEvent->COMMENT;
$properties["timezone"] = (string)$vEvent->DTSTART["TZID"];
$properties["organizer"] = (string)$vEvent->ORGANIZER;
$properties["busy"] = (string)$vEvent->{'X-MICROSOFT-CDO-INTENDEDSTATUS'}; // X-MICROSOFT-CDO-BUSYSTATUS
$properties["transp"] = (string)$vEvent->TRANSP;
//$properties["trigger"] = (string)$vEvent->COMMENT;
$properties["priority"] = (string)$vEvent->PRIORITY;
$properties["class"] = (string)$vEvent->CLASS;
2016-06-20 20:00:44 +02:00
//$properties["label"] = (string)$vEvent->COMMENT;
2016-06-20 19:42:07 +02:00
$properties["lastmodified"] = (string)$vEvent->{'LAST-MODIFIED'};
$properties["created"] = (string)$vEvent->CREATED;
2016-06-20 20:00:44 +02:00
$properties["rrule"] = (string)$vEvent->RRULE;
// Attendees
$properties["attendees"] = array();
if(isset($vEvent->ATTENDEE) && count($vEvent->ATTENDEE) > 0) {
foreach($vEvent->ATTENDEE as $attendee) {
$properties["attendees"][] = array(
"name" => (string)$attendee["CN"],
"mail" => (string)$attendee,
"status" => (string)$attendee["PARTSTAT"],
"role" => (string)$attendee["ROLE"]
);
}
}
// Alarms
$properties["alarms"] = array();
if(isset($vEvent->VALARM) && count($vEvent->VALARM) > 0) {
foreach($vEvent->VALARM as $alarm) {
$properties["alarms"][] = array(
"description" => (string)$alarm->DESCRIPTION,
"trigger" => (string)$alarm->TRIGGER,
"type" => (string)$alarm->TRIGGER["VALUE"]
);
}
}
2016-06-20 19:42:07 +02:00
array_push($events, $properties);
}
return $events;
2016-06-20 16:19:05 +02:00
}
}
;
?>