diff --git a/lib/ext/rtf.php b/lib/ext/rtf.php new file mode 100644 index 0000000..fd27374 --- /dev/null +++ b/lib/ext/rtf.php @@ -0,0 +1,712 @@ + + http://josefine.ben.tuwien.ac.at/~mfischer/ + + Latest versions of this class can always be found at + http://josefine.ben.tuwien.ac.at/~mfischer/developing/php/rtf/rtfclass.phps + Testing suite is available at + http://josefine.ben.tuwien.ac.at/~mfischer/developing/php/rtf/ + + License: GPLv2 + + Specification: + http://msdn.microsoft.com/library/default.asp?URL=/library/specs/rtfspec.htm + + General Notes: + ============== + Unknown or unspupported control symbols are silently ignored + + Group stacking is still not supported :( + group stack logic implemented; however not really used yet + ===================================================================================================== + + It was modified by me (Andreas Brodowski) to allow compressed RTF being uncompressed by code I ported from + Java to PHP and adapted according the needs of Z-Push. + + Currently it is being used to detect empty RTF Streams from Nokia Phones in MfE Clients + + It needs to be used by other backend writers that needs to have notes in calendar, appointment or tasks + objects to be written to their databases since devices send them usually in RTF Format... With Zarafa + you can write them directly to DB and Zarafa is doing the conversion job. Other Groupware systems usually + don't have this possibility... + + Aleksander Machniak fixed some deprecated function usage and some small issues +*/ + + +class rtf +{ + const LZRTF_HDR_DATA = "{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New RomanCourier{\\colortbl\\red0\\green0\\blue0\n\r\\par \\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx"; + const LZRTF_HDR_LEN = 207; + + protected $CRC32_TABLE = array( + 0x00000000,0x77073096,0xEE0E612C,0x990951BA,0x076DC419,0x706AF48F,0xE963A535,0x9E6495A3, + 0x0EDB8832,0x79DCB8A4,0xE0D5E91E,0x97D2D988,0x09B64C2B,0x7EB17CBD,0xE7B82D07,0x90BF1D91, + 0x1DB71064,0x6AB020F2,0xF3B97148,0x84BE41DE,0x1ADAD47D,0x6DDDE4EB,0xF4D4B551,0x83D385C7, + 0x136C9856,0x646BA8C0,0xFD62F97A,0x8A65C9EC,0x14015C4F,0x63066CD9,0xFA0F3D63,0x8D080DF5, + 0x3B6E20C8,0x4C69105E,0xD56041E4,0xA2677172,0x3C03E4D1,0x4B04D447,0xD20D85FD,0xA50AB56B, + 0x35B5A8FA,0x42B2986C,0xDBBBC9D6,0xACBCF940,0x32D86CE3,0x45DF5C75,0xDCD60DCF,0xABD13D59, + 0x26D930AC,0x51DE003A,0xC8D75180,0xBFD06116,0x21B4F4B5,0x56B3C423,0xCFBA9599,0xB8BDA50F, + 0x2802B89E,0x5F058808,0xC60CD9B2,0xB10BE924,0x2F6F7C87,0x58684C11,0xC1611DAB,0xB6662D3D, + 0x76DC4190,0x01DB7106,0x98D220BC,0xEFD5102A,0x71B18589,0x06B6B51F,0x9FBFE4A5,0xE8B8D433, + 0x7807C9A2,0x0F00F934,0x9609A88E,0xE10E9818,0x7F6A0DBB,0x086D3D2D,0x91646C97,0xE6635C01, + 0x6B6B51F4,0x1C6C6162,0x856530D8,0xF262004E,0x6C0695ED,0x1B01A57B,0x8208F4C1,0xF50FC457, + 0x65B0D9C6,0x12B7E950,0x8BBEB8EA,0xFCB9887C,0x62DD1DDF,0x15DA2D49,0x8CD37CF3,0xFBD44C65, + 0x4DB26158,0x3AB551CE,0xA3BC0074,0xD4BB30E2,0x4ADFA541,0x3DD895D7,0xA4D1C46D,0xD3D6F4FB, + 0x4369E96A,0x346ED9FC,0xAD678846,0xDA60B8D0,0x44042D73,0x33031DE5,0xAA0A4C5F,0xDD0D7CC9, + 0x5005713C,0x270241AA,0xBE0B1010,0xC90C2086,0x5768B525,0x206F85B3,0xB966D409,0xCE61E49F, + 0x5EDEF90E,0x29D9C998,0xB0D09822,0xC7D7A8B4,0x59B33D17,0x2EB40D81,0xB7BD5C3B,0xC0BA6CAD, + 0xEDB88320,0x9ABFB3B6,0x03B6E20C,0x74B1D29A,0xEAD54739,0x9DD277AF,0x04DB2615,0x73DC1683, + 0xE3630B12,0x94643B84,0x0D6D6A3E,0x7A6A5AA8,0xE40ECF0B,0x9309FF9D,0x0A00AE27,0x7D079EB1, + 0xF00F9344,0x8708A3D2,0x1E01F268,0x6906C2FE,0xF762575D,0x806567CB,0x196C3671,0x6E6B06E7, + 0xFED41B76,0x89D32BE0,0x10DA7A5A,0x67DD4ACC,0xF9B9DF6F,0x8EBEEFF9,0x17B7BE43,0x60B08ED5, + 0xD6D6A3E8,0xA1D1937E,0x38D8C2C4,0x4FDFF252,0xD1BB67F1,0xA6BC5767,0x3FB506DD,0x48B2364B, + 0xD80D2BDA,0xAF0A1B4C,0x36034AF6,0x41047A60,0xDF60EFC3,0xA867DF55,0x316E8EEF,0x4669BE79, + 0xCB61B38C,0xBC66831A,0x256FD2A0,0x5268E236,0xCC0C7795,0xBB0B4703,0x220216B9,0x5505262F, + 0xC5BA3BBE,0xB2BD0B28,0x2BB45A92,0x5CB36A04,0xC2D7FFA7,0xB5D0CF31,0x2CD99E8B,0x5BDEAE1D, + 0x9B64C2B0,0xEC63F226,0x756AA39C,0x026D930A,0x9C0906A9,0xEB0E363F,0x72076785,0x05005713, + 0x95BF4A82,0xE2B87A14,0x7BB12BAE,0x0CB61B38,0x92D28E9B,0xE5D5BE0D,0x7CDCEFB7,0x0BDBDF21, + 0x86D3D2D4,0xF1D4E242,0x68DDB3F8,0x1FDA836E,0x81BE16CD,0xF6B9265B,0x6FB077E1,0x18B74777, + 0x88085AE6,0xFF0F6A70,0x66063BCA,0x11010B5C,0x8F659EFF,0xF862AE69,0x616BFFD3,0x166CCF45, + 0xA00AE278,0xD70DD2EE,0x4E048354,0x3903B3C2,0xA7672661,0xD06016F7,0x4969474D,0x3E6E77DB, + 0xAED16A4A,0xD9D65ADC,0x40DF0B66,0x37D83BF0,0xA9BCAE53,0xDEBB9EC5,0x47B2CF7F,0x30B5FFE9, + 0xBDBDF21C,0xCABAC28A,0x53B39330,0x24B4A3A6,0xBAD03605,0xCDD70693,0x54DE5729,0x23D967BF, + 0xB3667A2E,0xC4614AB8,0x5D681B02,0x2A6F2B94,0xB40BBE37,0xC30C8EA1,0x5A05DF1B,0x2D02EF8D, + ); + + protected $rtf; // rtf core stream + protected $rtf_len; // length in characters of the stream (get performace due avoiding calling strlen everytime) + + protected $wantXML; // convert to XML + protected $wantHTML; // convert to HTML + protected $wantASCII; // convert to HTML + protected $styles = array(); // if wantHTML, stylesheet definitions are put in here + + // the only variable which should be accessed from the outside + public $out; // output data stream (depends on which $wantXXXXX is set to true + public $outstyles; // htmlified styles (generated after parsing if wantHTML + public $err = array(); // array of error message, no entities on no error + + // internal parser variables + // control word variables + protected $cword; // holds the current (or last) control word, depending on $cw + protected $cw; // are we currently parsing a control word ? + protected $cfirst; // could this be the first character ? so watch out for control symbols + protected $flags = array(); // parser flags + protected $queue; // every character which is no sepcial char, not belongs to a control word/symbol; is generally considered being 'plain' + protected $stack = array(); // group stack + + /* keywords which don't follow the specification (used by Word '97 - 2000) */ + // not yet used + protected $control_exception = array( + "clFitText", + "clftsWidth(-?[0-9]+)?", + "clNoWrap(-?[0-9]+)?", + "clwWidth(-?[0-9]+)?", + "tdfrmtxtBottom(-?[0-9]+)?", + "tdfrmtxtLeft(-?[0-9]+)?", + "tdfrmtxtRight(-?[0-9]+)?", + "tdfrmtxtTop(-?[0-9]+)?", + "trftsWidthA(-?[0-9]+)?", + "trftsWidthB(-?[0-9]+)?", + "trftsWidth(-?[0-9]+)?", + "trwWithA(-?[0-9]+)?", + "trwWithB(-?[0-9]+)?", + "trwWith(-?[0-9]+)?", + "spectspecifygen(-?[0-9]+)?", + ); + + protected $charset_table = array( + "0" => "ANSI", + "1" => "Default", + "2" => "Symbol", + "77" => "Mac", + "128" => "Shift Jis", + "129" => "Hangul", + "130" => "Johab", + "134" => "GB2312", + "136" => "Big5", + "161" => "Greek", + "162" => "Turkish", + "163" => "Vietnamese", + "177" => "Hebrew", + "178" => "Arabic", + "179" => "Arabic Traditional", + "180" => "Arabic user", + "181" => "Hebrew user", + "186" => "Baltic", + "204" => "Russian", + "222" => "Thai", + "238" => "Eastern European", + "255" => "PC 437", + "255" => "OEM", + ); + + /* note: the only conversion table used */ + protected $fontmodifier_table = array( + "bold" => "b", + "italic" => "i", + "underlined" => "u", + "strikethru" => "strike", + ); + + + function __construct($data) + { + $this->rtf_len = 0; + $this->rtf = ''; + + if ($data) { + $this->loadrtf($data); + } + } + + // loadrtf - load the raw rtf data to be converted by this class + // data = the raw rtf + function loadrtf($data) + { + if ($this->rtf = $this->uncompress($data)) { + $this->rtf_len = strlen($this->rtf); + } + + if (!$this->rtf_len) { + return false; + } + + return true; + } + + // uncompress - uncompress compressed rtf data + // src = the compressed raw rtf in LZRTF format + function uncompress($src) + { + $header = unpack("LcSize/LuSize/Lmagic/Lcrc32",substr($src,0,16)); + $in = 16; + + if ($header['cSize'] != strlen($src)-4) { +// debugLog("Stream too short"); + return false; + } + + if ($header['crc32'] != $this->LZRTFCalcCRC32($src, 16, (($header['cSize']+4))-16)) { +// debugLog("CRC MISMATCH"); + return false; + } + + if ($header['magic'] == 0x414c454d) { // uncompressed RTF - return as is. + $dest = substr($src, $in, $header['uSize']); + } + else if ($header['magic'] == 0x75465a4c) { // compressed RTF - uncompress. + $dst = self::LZRTF_HDR_DATA; + $out = self::LZRTF_HDR_LEN; + $oblen = self::LZRTF_HDR_LEN + $header['uSize']; + $flags = 0; + $flagCount = 0; + + while ($out < $oblen) { + $flags = ($flagCount++ % 8 == 0) ? ord($src{$in++}) : $flags >> 1; + if (($flags & 1) == 1) { + $offset = ord($src{$in++}); + $length = ord($src{$in++}); + $offset = ($offset << 4) | ($length >> 4); + $length = ($length & 0xF) + 2; + $offset = (int)($out / 4096) * 4096 + $offset; + if ($offset >= $out) $offset -= 4096; + $end = $offset + $length; + while ($offset < $end) { + $dst[$out++] = $dst[$offset++]; + }; + } + else { + $dst[$out++] = $src[$in++]; + } + } + + $src = $dst; + $dest = substr($src, self::LZRTF_HDR_LEN, $header['uSize']); + } + else { // unknown magic - returfn false (please report if this ever happens) +// debugLog("Unknown Magic"); + return false; + } + + return $dest; + } + + // LZRTFCalcCRC32 - calculates the CRC32 of the LZRTF data part + // buf = the whole rtf data part + // off = start point of crc calculation + // len = length of data to calculate CRC for + // function is necessary since in RTF there is no XOR 0xffffffff being done (said to be 0x00 unsafe CRC32 calculation + function LZRTFCalcCRC32($buf, $off, $len) + { + $c = 0; + $end = $off + $len; + + for($i = $off; $i < $end; $i++) { + $c = $this->CRC32_TABLE[($c ^ ord($buf[$i])) & 0xFF] ^ (($c >> 8) & 0x00ffffff); + } + + return $c; + } + + function parserInit($type) + { + // Default values according to the specs + $this->flags = array( + "fontsize" => 24, + "beginparagraph" => true, + ); + + $this->cw = false; // flag if control word is currently parsed + $this->cfirst = false; // first control character ? + $this->cword = ""; // last or current control word (depends on $this->cw) + $this->queue = ""; // plain text data found during parsing + $this->out = ''; + $this->wantASCII = false; + $this->wantXML = false; + $this->wantHTML = false; + + switch (strtolower($type)) { + case "xml": $this->wantXML = true; break; + case "html": $this->wantHTML = true; break; + case "ascii": + case "plain": + default: $this->wantASCII = true; break; + } + } + + function parseControl($control, $parameter) + { + switch ($control) { + case "fonttbl": // font table definition start + $this->flags["fonttbl"] = true; // signal fonttable control words they are allowed to behave as expected + break; + case "f": // define or set font + if ($this->flags["fonttbl"]) { // if its set, the fonttable definition is written to; else its read from + $this->flags["fonttbl_current_write"] = (string) $parameter; + } + else { + $this->flags["fonttbl_current_read"] = (string) $parameter; + } + break; + case "fcharset": // this is for preparing flushQueue; it then moves the Queue to $this->fonttable .. instead to formatted output + $this->flags["fonttbl_want_fcharset"] = $parameter; + break; + case "fs": // sets the current fontsize; is used by stylesheets (which are therefore generated on the fly + $this->flags["fontsize"] = $parameter; + break; + + case "qc": // handle center alignment + $this->flags["alignment"] = "center"; + break; + case "qr": // handle right alignment + $this->flags["alignment"] = "right"; + break; + + case "pard": // reset paragraph settings (only alignment) + $this->flags["alignment"] = ""; + break; + case "par": // define new paragraph (for now, thats a simple break in html) begin new line + if ($this->wantHTML) { + $this->out .= "
"; + } + else if ($this->wantASCII) { + $this->out .= "\n"; + } + $this->flags["beginparagraph"] = true; + break; + + case "bnone": // bold + $parameter = "0"; + case "b": + // haven'y yet figured out WHY I need a (string)-cast here ... hm + $this->flags["bold"] = strval($parameter) !== "0"; + break; + + case "ulnone": // underlined + $parameter = "0"; + case "ul": + $this->flags["underline"] = strval($parameter) !== "0"; + break; + + case "inone": // italic + $parameter = "0"; + case "i": + $this->flags["italic"] = strval($parameter) !== "0"; + break; + + case "strikenone": // strikethru + $parameter = "0"; + case "strike": + $this->flags["strikethru"] = strval($parameter) !== "0"; + break; + + case "plain": // reset all font modifiers and fontsize to 12 + $this->flags["bold"] = false; + $this->flags["italic"] = false; + $this->flags["underlined"] = false; + $this->flags["strikethru"] = false; + $this->flags["fontsize"] = 12; + $this->flags["subscription"] = false; + $this->flags["superscription"] = false; + break; + + case "subnone": // subscription + $parameter = "0"; + case "sub": + $this->flags["subscription"] = strval($parameter) !== "0"; + break; + + case "supernone": // superscription + $parameter = "0"; + case "super": + $this->flags["superscription"] = strval($parameter) !== "0"; + break; + } + } + + /* + Dispatch the control word to the output stream + */ + function flushControl() + { + if (preg_match("/^([A-Za-z]+)(-?[0-9]*) ?$/", $this->cword, $match)) { + $this->parseControl($match[1], $match[2]); + if ($this->wantXML) { + $this->out .= " 0) { + $this->out .= " param=\"".$match[2]."\""; + } + $this->out .= "/>"; + } + } + } + + /* + If output stream supports comments, dispatch it + */ + function flushComment($comment) + { + if ($this->wantXML || $this->wantHTML) { + $this->out .= ""; + } + } + + /* + Dispatch start/end of logical rtf groups (not every output type needs it; merely debugging purpose) + */ + function flushGroup($state) + { + if ($state == "open") { /* push onto the stack */ + array_push($this->stack, $this->flags); + + if ($this->wantXML) { + $this->out .= ""; + } + } + + if ($state == "close") { /* pop from the stack */ + $this->last_flags = $this->flags; + $this->flags = array_pop($this->stack); + $this->flags["fonttbl_current_write"] = ""; // on group close, no more fontdefinition will be written to this id + // this is not really the right way to do it ! + // of course a '}' not necessarily donates a fonttable end; a fonttable + // group at least *can* contain sub-groups + // therefore an stacked approach is heavily needed + $this->flags["fonttbl"] = false; // no matter what you do, if a group closes, its fonttbl definition is closed too + + if ($this->wantXML) { + $this->out .= ""; + } + } + } + + function flushHead() + { + if ($this->wantXML) { + $this->out .= ""; + } + } + + function flushBottom() + { + if ($this->wantXML) { + $this->out .= ""; + } + } + + function checkHtmlSpanContent($command) + { + reset($this->fontmodifier_table); + while (list($rtf, $html) = each($this->fontmodifier_table)) { + if ($this->flags[$rtf]) { + $this->out .= $command == "start" ? "<$html>" : ""; + } + } + } + + /* + flush text in queue + */ + function flushQueue() + { + if (strlen($this->queue)) { + // processing logic + if (isset($this->flags["fonttbl_want_fcharset"]) && + preg_match("/^[0-9]+$/", $this->flags["fonttbl_want_fcharset"]) + ) { + $this->fonttable[$this->flags["fonttbl_want_fcharset"]]["charset"] = $this->queue; + $this->flags["fonttbl_want_fcharset"] = ""; + $this->queue = ""; + } + + // output logic + if (strlen($this->queue)) { + // Everything which passes this is (or, at least, *should*) be only outputted plaintext + // Thats why we can safely add the css-stylesheet when using wantHTML + if ($this->wantXML) { + $this->out .= "" . $this->queue . ""; + } + else if ($this->wantHTML && isset($this->flags["fonttbl_current_read"])) { + // only output html if a valid (for now, just numeric) fonttable is given + if (preg_match("/^[0-9]+$/", $this->flags["fonttbl_current_read"])) { + if ($this->flags["beginparagraph"]) { + $this->flags["beginparagraph"] = false; + $this->out .= "
flags["alignment"]) { + case "right": + $this->out .= "right"; + break; + case "center": + $this->out .= "center"; + break; + case "left": + default: + $this->out .= "left"; + } + $this->out .= "\">"; + } + + $class_name = "f".$this->flags["fonttbl_current_read"]."s".$this->flags["fontsize"]; + /* define new style for that span */ + $this->styles[$class_name] = "font-family: ".$this->fonttable[$this->flags["fonttbl_current_read"]]["charset"]."; font-size: ".$this->flags["fontsize"].";"; + /* write span start */ + $this->out .= ""; + /* check if the span content has a modifier */ + $this->checkHtmlSpanContent("start"); + /* write span content */ + $this->out .= $this->queue; + /* close modifiers */ + $this->checkHtmlSpanContent("stop"); + /* close span */ + $this->out .= ""; + } + } + + $this->queue = ""; + } + } + } + + /* + handle special charactes + */ + function flushSpecial($special) + { + if (strlen($special) == 2) { + if ($this->wantASCII) { + $this->out .= chr(hexdec('0x'.$special)); + } + else if ($this->wantXML) { + $this->out .= ""; + } + else if ($this->wantHTML) { + $this->out .= htmlentities(chr(hexdec('0x' . $special))); + } + } + } + + /* + Output errors at end + */ + function flushErrors() + { + if (count($this->err) > 0) { + if ($this->wantXML) { + $this->out .= ""; + while (list($num,$value) = each($this->err)) { + $this->out .= "$value"; + } + $this->out .= ""; + } + } + } + + function makeStyles() + { + if (empty($this->styles)) { + return $this->outstyles = ''; + } + + $this->outstyles = "\n"; + + return $this->outstyles; + } + + function parse($type) + { + $i = 0; + + $this->parserInit($type); + $this->flushHead(); + + while ($i < $this->rtf_len) { + switch ($this->rtf[$i]) { + case "{": + if ($this->cw) { + $this->flushControl(); + $this->cw = false; + $this->cfirst = false; + } + else { + $this->flushQueue(); + } + $this->flushGroup("open"); + break; + + case "}": + if ($this->cw) { + $this->flushControl(); + $this->cw = false; + $this->cfirst = false; + } + else { + $this->flushQueue(); + } + $this->flushGroup("close"); + break; + + case "\\": + if ($this->cfirst) { // catches '\\' + $this->queue .= "\\"; // replaced single quotes + $this->cfirst = false; + $this->cw = false; + break; + } + if ($this->cw) { + $this->flushControl(); + } + else { + $this->flushQueue(); + } + $this->cw = true; + $this->cfirst = true; + $this->cword = ""; + break; + + default: + if ((ord($this->rtf[$i]) == 10) || (ord($this->rtf[$i]) == 13)) { + break; // eat line breaks + } + + if ($this->cw) { // active control word ? + /* + Watch the RE: there's an optional space at the end which IS part of + the control word (but actually its ignored by flushControl) + */ + if(preg_match("/^[a-zA-Z0-9-]?$/", $this->rtf[$i])) { // continue parsing + $this->cword .= $this->rtf[$i]; + $this->cfirst = false; + } else { + /* + Control word could be a 'control symbol', like \~ or \* etc. + */ + $specialmatch = false; + if ($this->cfirst) { + if($this->rtf[$i] == '\'') { // expect to get some special chars + $this->flushQueue(); + $this->flushSpecial($this->rtf[$i+1].$this->rtf[$i+2]); + $i+=2; + $specialmatch = true; + $this->cw = false; + $this->cfirst = false; + $this->cword = ""; + } else + if(preg_match("/^[{}\*]$/", $this->rtf[$i])) { + $this->flushComment("control symbols not yet handled"); + $specialmatch = true; + } + $this->cfirst = false; + } else { + if($this->rtf[$i] == ' ') { // space delimtes control words, so just discard it and flush the controlword + $this->cw = false; + $this->flushControl(); + break; + } + } + if (!$specialmatch) { + $this->flushControl(); + $this->cw = false; + $this->cfirst = false; + /* + The current character is a delimeter, but is NOT + part of the control word so we hop one step back + in the stream and process it again + */ + $i--; + } + } + } + else { + // < and > need translation before putting into queue when XML or HTML is wanted + if ($this->wantHTML || $this->wantXML) { + switch ($this->rtf[$i]) { + case "<": + $this->queue .= "<"; + break; + case ">": + $this->queue .= ">"; + break; + default: + $this->queue .= $this->rtf[$i]; + break; + } + } + else { + $this->queue .= $this->rtf[$i]; + } + } + } + $i++; + } + + $this->flushQueue(); + $this->flushErrors(); + $this->flushBottom(); + + if ($this->wantHTML) { + $style = $this->makeStyles(); + + return '' . $style . $this->out . ''; + } + + return $this->out; + } +} diff --git a/lib/filter/mapistore/common.php b/lib/filter/mapistore/common.php index 7f81655..0429820 100644 --- a/lib/filter/mapistore/common.php +++ b/lib/filter/mapistore/common.php @@ -1,1065 +1,1136 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_common { // Common properties [MS-OXCMSG] protected static $common_map = array( // 'PidTagAccess' => '', // 'PidTagAccessLevel' => '', // 0 - read-only, 1 - modify // 'PidTagChangeKey' => '', 'PidTagCreationTime' => 'creation-date', // PtypTime, UTC 'PidTagLastModificationTime' => 'last-modification-date', // PtypTime, UTC // 'PidTagLastModifierName' => '', // 'PidTagObjectType' => '', // @TODO 'PidTagHasAttachments' => 'attach', // PtypBoolean // 'PidTagRecordKey' => '', // 'PidTagSearchKey' => '', 'PidNameKeywords' => 'categories', ); protected $recipient_track_status_map = array( 'TENTATIVE' => 0x00000002, 'ACCEPTED' => 0x00000003, 'DECLINED' => 0x00000004, ); protected $recipient_type_map = array( 'NON-PARTICIPANT' => 0x00000004, 'OPT-PARTICIPANT' => 0x00000002, 'REQ-PARTICIPANT' => 0x00000001, 'CHAIR' => 0x00000001, ); /** * Mapping of weekdays */ protected static $recurrence_day_map = array( 'SU' => 0x00000000, 'MO' => 0x00000001, 'TU' => 0x00000002, 'WE' => 0x00000003, 'TH' => 0x00000004, 'FR' => 0x00000005, 'SA' => 0x00000006, 'BYDAY-SU' => 0x00000001, 'BYDAY-MO' => 0x00000002, 'BYDAY-TU' => 0x00000004, 'BYDAY-WE' => 0x00000008, 'BYDAY-TH' => 0x00000010, 'BYDAY-FR' => 0x00000020, 'BYDAY-SA' => 0x00000040, ); /** * Extracts data from kolab data array */ public static function get_kolab_value($data, $name) { $name_items = explode('.', $name); $count = count($name_items); $value = $data[$name_items[0]]; // special handling of x-custom properties if ($name_items[0] === 'x-custom') { foreach ((array) $value as $custom) { if ($custom['identifier'] === $name_items[1]) { return $custom['value']; } } return null; } for ($i = 1; $i < $count; $i++) { if (!is_array($value)) { return null; } list($key, $num) = explode(':', $name_items[$i]); $value = $value[$key]; if ($num !== null && $value !== null) { $value = is_array($value) ? $value[$num] : null; } } return $value; } /** * Sets specified kolab data item */ public static function set_kolab_value(&$data, $name, $value) { $name_items = explode('.', $name); $count = count($name_items); $element = &$data; // x-custom properties if ($name_items[0] === 'x-custom') { // this is supposed to be converted later by parse_common_props() $data[$name] = $value; return; } if ($count > 1) { for ($i = 0; $i < $count - 1; $i++) { $key = $name_items[$i]; if (!array_key_exists($key, $element)) { $element[$key] = array(); } $element = &$element[$key]; } } $element[$name_items[$count - 1]] = $value; } /** * Parse common properties in object data (convert into MAPI format) */ protected function parse_common_props(&$result, $data, $context = array()) { if (empty($context)) { // @TODO: throw exception? return; } if ($data['uid'] && $context['folder_uid']) { $result['id'] = kolab_api_filter_mapistore::uid_encode($context['folder_uid'], $data['uid']); } if ($context['folder_uid']) { $result['parent_id'] = $context['folder_uid']; } foreach (self::$common_map as $mapi_idx => $kolab_idx) { if (!isset($result[$mapi_idx]) && ($value = $data[$kolab_idx]) !== null) { switch ($mapi_idx) { case 'PidTagCreationTime': case 'PidTagLastModificationTime': $result[$mapi_idx] = self::date_php2mapi($value, true); break; case 'PidTagHasAttachments': if (!empty($value) && $this->model != 'note') { $result[$mapi_idx] = true; } break; case 'PidNameKeywords': $result[$mapi_idx] = self::parse_categories((array) $value); break; } } } } /** * Convert common properties into kolab format */ protected function convert_common_props(&$result, $data, $original) { // @TODO: id, parent_id? foreach (self::$common_map as $mapi_idx => $kolab_idx) { if (array_key_exists($mapi_idx, $data) && !array_key_exists($kolab_idx, $result)) { $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $dt = self::date_mapi2php($value); $result[$kolab_idx] = $dt->format('Y-m-d\TH:i:s\Z'); } break; default: if ($value) { $result[$kolab_idx] = $value; } break; } } } // Handle x-custom fields foreach ((array) $result as $key => $value) { if (strpos($key, 'x-custom.') === 0) { unset($result[$key]); $key = substr($key, 9); foreach ((array) $original['x-custom'] as $idx => $custom) { if ($custom['identifier'] == $key) { if ($value) { $original['x-custom'][$idx]['value'] = $value; } else { unset($original['x-custom'][$idx]); } $x_custom_update = true; continue 2; } } if ($value) { $original['x-custom'][] = array( 'identifier' => $key, 'value' => $value, ); } $x_custom_update = true; } } if ($x_custom_update) { $result['x-custom'] = array_values($original['x-custom']); } } /** * Filter property names with mapping (kolab <> MAPI) * * @param array $attrs Property names * @param bool $reverse Reverse mapping * * @return array Property names */ public function attributes_filter($attrs, $reverse = false) { $map = array_merge(self::$common_map, $this->map()); $result = array(); // add some special common attributes $map['PidTagMessageClass'] = 'PidTagMessageClass'; $map['collection'] = 'collection'; $map['id'] = 'uid'; foreach ($attrs as $attr) { if ($reverse) { if ($name = array_search($attr, $map)) { $result[] = $name; } } else if ($name = $map[$attr]) { $result[] = $name; } } return $result; } /** * Return properties map */ protected function map() { return array(); } /** * Parse categories according to [MS-OXCICAL 2.1.3.1.1.20.3] * * @param array Categories * * @return array Categories */ public static function parse_categories($categories) { if (!is_array($categories)) { return; } $result = array(); foreach ($categories as $idx => $val) { $val = preg_replace('/(\x3B|\x2C|\x06\x1B|\xFE\x54|\xFF\x1B)/', '', $val); $val = preg_replace('/\s+/', ' ', $val); $val = trim($val); $len = mb_strlen($val); if ($len) { if ($len > 255) { $val = mb_substr($val, 0, 255); } $result[mb_strtolower($val)] = $val; } } return array_values($result); } /** * Convert Kolab 'attendee' specification into MAPI recipient * and add it to the result */ protected function attendee_to_recipient($attendee, &$result, $is_organizer = false) { $email = $attendee['cal-address']; $params = (array) $attendee['parameters']; // parse mailto string if (strpos($email, 'mailto:') === 0) { $email = urldecode(substr($email, 7)); } $emails = rcube_mime::decode_address_list($email, 1); if (!empty($email)) { $email = $emails[key($emails)]; $recipient = array( 'PidTagAddressType' => 'SMTP', 'PidTagDisplayName' => $params['cn'] ?: $email['name'], 'PidTagDisplayType' => 0, 'PidTagEmailAddress' => $email['mailto'], ); if ($is_organizer) { $recipient['PidTagRecipientFlags'] = 0x00000003; $recipient['PidTagRecipientType'] = 0x00000001; } else { $recipient['PidTagRecipientFlags'] = 0x00000001; $recipient['PidTagRecipientTrackStatus'] = (int) $this->recipient_track_status_map[$params['partstat']]; $recipient['PidTagRecipientType'] = $this->to_recipient_type($params['cutype'], $params['role']); } $recipient['PidTagRecipientDisplayName'] = $recipient['PidTagDisplayName']; $result['recipients'][] = $recipient; if (strtoupper($params['rsvp']) == 'TRUE') { $result['PidTagReplyRequested'] = true; $result['PidTagResponseRequested'] = true; } } } /** * Convert MAPI recipient into Kolab attendee */ protected function recipient_to_attendee($recipient, &$result) { if ($email = $recipient['PidTagEmailAddress']) { $mailto = 'mailto:' . rawurlencode($email); $attendee = array( 'cal-address' => $mailto, 'parameters' => array( 'cn' => $recipient['PidTagDisplayName'] ?: $recipient['PidTagRecipientDisplayName'], ), ); if ($recipient['PidTagRecipientFlags'] == 0x00000003) { $result['organizer'] = $attendee; } else { switch ($recipient['PidTagRecipientType']) { case 0x00000004: $role = 'NON-PARTICIPANT'; break; case 0x00000003: $cutype = 'RESOURCE'; break; case 0x00000002: $role = 'OPT-PARTICIPANT'; break; case 0x00000001: $role = 'REQ-PARTICIPANT'; break; } $map = array_flip($this->recipient_track_status_map); $partstat = $map[$recipient['PidTagRecipientTrackStatus']] ?: 'NEEDS-ACTION'; // @TODO: rsvp? $attendee['parameters']['cutype'] = $cutype; $attendee['parameters']['role'] = $role; $attendee['parameters']['partstat'] = $partstat; $result['attendee'][] = $attendee; } } } /** * Convert Kolab valarm specification into MAPI properties * - * @param array $data Kolab object - * @param array $result Object data (MAPI format) + * @param array $data Kolab object + * @param array $result Object data (MAPI format) */ protected function alarm_from_kolab($data, &$result) { // [MS-OXCICAL] 2.1.3.1.1.20.62 foreach ((array) $data['valarm'] as $alarm) { if (!empty($alarm['properties']) && $alarm['properties']['action'] != 'DISPLAY') { continue; } // @TODO alarms with Date-Time instead of Duration $trigger = $alarm['properties']['trigger']; if ($trigger['duration'] && $trigger['parameters']['related'] != 'END' && ($delta = self::reminder_duration_to_delta($trigger['duration'])) ) { // Find next instance of the appointment (in UTC) $now = kolab_api::$now ?: new DateTime('now', new DateTimeZone('UTC')); if ($data['dtstart']) { $dtstart = kolab_api_input_json::to_datetime($data['dtstart']); // check if start date is from the future if ($dtstart > $now) { $reminder_time = $dtstart; } // find next occurence else { kolab_api_input_json::parse_recurrence($data, $res); if (!empty($res['recurrence'])) { $recurlib = libcalendaring::get_recurrence(); $recurlib->init($res['recurrence'], $now); $next = $recurlib->next(); if ($next) { $reminder_time = $next; } } } } $result['PidLidReminderDelta'] = $delta; // If all instances are in the past, don't set ReminderTime nor ReminderSet if ($reminder_time) { $signal_time = clone $reminder_time; $signal_time->sub(new DateInterval('PT' . $delta . 'M')); $result['PidLidReminderSet'] = true; $result['PidLidReminderTime'] = $this->date_php2mapi($reminder_time, true); $result['PidLidReminderSignalTime'] = $this->date_php2mapi($signal_time, true); } // MAPI supports only one alarm break; } } } /** * Convert MAPI recurrence into Kolab (MS-OXICAL: 2.1.3.2.2) * * @param string $data MAPI object * @param array $result Kolab object */ protected function alarm_to_kolab($data, &$result) { if ($data['PidLidReminderSet'] && ($delta = $data['PidLidReminderDelta'])) { $duration = self::reminder_delta_to_duration($delta); $alarm = array( 'action' => 'DISPLAY', 'trigger' => array('duration' => $duration), // 'description' => 'Reminder', ); $result['valarm'] = array(array('properties' => $alarm)); } else if (array_key_exists('PidLidReminderSet', $data) || array_key_exists('PidLidReminderDelta', $data)) { $result['valarm'] = array(); } } /** * Convert PidLidReminderDelta value into xCal duration */ protected static function reminder_delta_to_duration($delta) { if ($delta == 0x5AE980E1) { $delta = 15; } $delta = (int) $delta; return "-PT{$delta}M"; } /** * Convert Kolab alarm duration into PidLidReminderDelta */ protected static function reminder_duration_to_delta($duration) { if ($duration && preg_match('/^-[PT]*([0-9]+)([WDHMS])$/', $duration, $matches)) { $value = intval($matches[1]); switch ($matches[2]) { case 'S': $value = intval(round($value/60)); break; case 'H': $value *= 60; break; case 'D': $value *= 24 * 60; break; case 'W': $value *= 7 * 24 * 60; break; } return $value; } } /** * Convert Kolab recurrence specification into MAPI properties * - * @param array $data Kolab object - * @param array $object Object data (MAPI format) + * @param array $data Kolab object + * @param array $object Object data (MAPI format) * * @return object MAPI recurrence in binary format */ protected function recurrence_from_kolab($data, $object = array()) { if ((empty($data['rrule']) || empty($data['rrule']['recur'])) && (empty($data['rdate']) || empty($data['rdate']['date'])) ) { return null; } $type = $this->model; // Get event/task start date for FirstDateTime calculations if ($dtstart = kolab_api_input_json::to_datetime($data['dtstart'])) { // StartDate: Set to the date portion of DTSTART, in the time zone specified // by PidLidTimeZoneStruct. This date is stored in minutes after // midnight Jan 1, 1601. Note that this value MUST always be // evenly divisible by 1440. // EndDate: Set to the start date of the last instance of a recurrence, in the // time zone specified by PidLidTimeZoneStruct. This date is // stored in minutes after midnight January 1, 1601. If the // recurrence is infinite, set EndDate to 0x5AE980DF. Note that // this value MUST always be evenly divisible by 1440, except for // the special value 0x5AE980DF. $startdate = clone $dtstart; $startdate->setTime(0, 0, 0); $startdate = self::date_php2mapi($startdate, true); $startdate = intval($startdate / 60); if ($mod = ($startdate % 1440)) { $startdate -= $mod; } // @TODO: get first occurrence of the event using libcalendaring_recurrence class ? } else { rcube::raise_error(array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Found recurring $type without start date, skipping recurrence", ), true, false); return; } $rule = (array) ($data['rrule'] ? $data['rrule']['recur'] : null); $result = array( 'Period' => $rule && $rule['interval'] ? $rule['interval'] : 1, 'FirstDOW' => self::day2bitmask($rule['wkst'] ?: 'MO'), 'OccurrenceCount' => 0x0000000A, 'StartDate' => $startdate, 'EndDate' => 0x5AE980DF, 'FirstDateTime' => $startdate, 'CalendarType' => kolab_api_filter_mapistore_structure_recurrencepattern::CALENDARTYPE_DEFAULT, 'ModifiedInstanceDates' => array(), 'DeletedInstanceDates' => array(), ); switch ($rule['freq']) { case 'DAILY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_DAILY; $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY; $result['Period'] *= 1440; break; case 'WEEKLY': // if BYDAY does not exist use day from DTSTART if (empty($rule['byday'])) { $rule['byday'] = strtoupper($startdate->format('S')); } $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_WEEKLY; $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_WEEK; $result['PatternTypeSpecific'] = self::day2bitmask($rule['byday'], 'BYDAY-'); break; case 'MONTHLY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_MONTHLY; if (!empty($rule['bymonthday'])) { // MAPI doesn't support multi-valued month days $month_day = min(explode(',', $rule['bymonthday'])); $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTH; $result['PatternTypeSpecific'] = $month_day == -1 ? 0x0000001F : $month_day; } else { $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); if (!empty($rule['bysetpos'])) { $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; } } break; case 'YEARLY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_YEARLY; $result['Period'] *= 12; // MAPI doesn't support multi-valued months if ($rule['bymonth']) { // @TODO: set $startdate } if (!empty($rule['bymonthday'])) { // MAPI doesn't support multi-valued month days $month_day = min(explode(',', $rule['bymonthday'])); $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'] = array(0, $month_day == -1 ? 0x0000001F : $month_day); } else { $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); if (!empty($rule['bysetpos'])) { $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; } } break; } $exception_info = array(); $extended_exception = array(); // Custom occurrences (RDATE) if (!empty($data['rdate'])) { foreach ((array) $data['rdate']['date'] as $dt) { try { $dt = new DateTime($dt, $dtstart->getTimezone()); $dt->setTime(0, 0, 0); $dt = self::date_php2minutes($dt); $result['ModifiedInstanceDates'][] = $dt; $result['DeletedInstanceDates'][] = $dt; $exception_info[] = new kolab_api_filter_mapistore_structure_exceptioninfo(array( 'StartDateTime' => $dt, 'EndDateTime' => $dt + $object['PidLidAppointmentDuration'], 'OriginalStartDate' => $dt, 'OverrideFlags' => 0, )); $extended_exception[] = kolab_api_filter_mapistore_structure_extendedexception::get_empty(); } catch (Exception $e) { } } $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC; $result['OccurenceCount'] = count($result['ModifiedInstanceDates']); // @FIXME: Kolab format says there can be RDATE and/or RRULE // MAPI specification says there must be RRULE if RDATE is specified if (!$result['RecurFrequency']) { $result['RecurFrequency'] = 0; $result['PatternType'] = 0; } } if ($rule && !empty($rule['until'])) { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_AFTER; } else if ($rule && !empty($rule['count'])) { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC; $result['OccurrenceCount'] = $rule['count']; } else if (!isset($result['EndType'])) { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NEVER; } // calculate EndDate if ($rule && $result['EndType'] != kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NEVER) { kolab_api_input_json::parse_recurrence($data, $res); if (!empty($res['recurrence'])) { try { $recurlib = libcalendaring::get_recurrence(); $recurlib->init($res['recurrence'], $dtstart); $end = $recurlib->end(); if ($end) { $result['EndDate'] = intval(self::date_php2mapi($end) / 60); } } catch (Exception $e) { rcube::raise_error($e, true, false); } } } // Deleted instances (EXDATE) if (!empty($data['exdate'])) { if (!empty($data['exdate']['date'])) { $exceptions = (array) $data['exdate']['date']; } else if (!empty($data['exdate']['date-time'])) { $exceptions = (array) $data['exdate']['date-time']; } else { $exceptions = array(); } // convert date(-time)s to numbers foreach ($exceptions as $idx => $dt) { try { $dt = new DateTime($dt, $dtstart->getTimezone()); $dt->setTime(0, 0, 0); $result['DeletedInstanceDates'][] = self::date_php2minutes($dt); } catch (Exception $e) { } } } // [MS-OXCICAL] 2.1.3.1.1.20.13: Sort and make exceptions valid foreach (array('DeletedInstanceDates', 'ModifiedInstanceDates') as $key) { if (!empty($result[$key])) { sort($result[$key]); $result[$key] = array_values(array_unique(array_filter($result[$key]))); } } $result = new kolab_api_filter_mapistore_structure_recurrencepattern($result); if ($type == 'task') { return $result->output(true); } // @TODO: exceptions $byhour = $rule['byhour'] ? min(explode(',', $rule['byhour'])) : 0; $byminute = $rule['byminute'] ? min(explode(',', $rule['byminute'])) : 0; $offset = 60 * intval($byhour) + intval($byminute); $arp = array( 'RecurrencePattern' => $result, 'StartTimeOffset' => $offset, 'EndTimeOffset' => $offset + $object['PidLidAppointmentDuration'], 'ExceptionInfo' => $exception_info, 'ExtendedException' => $extended_exception, ); $result = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern($arp); return $result->output(true); } /** * Convert MAPI recurrence into Kolab (MS-OXICAL: 2.1.3.2.2) * * @param string $rule MAPI binary representation of recurrence rule * @param array $object Kolab object */ protected function recurrence_to_kolab($rule, &$object) { if (empty($rule)) { return array(); } // parse binary (Appointment)RecurrencePattern if ($this->model == 'event') { $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern(); $arp->input($rule, true); $rp = $arp->RecurrencePattern; } else { $rp = new kolab_api_filter_mapistore_structure_recurrencepattern(); $rp->input($rule, true); } $result = array( 'interval' => $rp->Period, ); switch ($rp->PatternType) { case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY: $result['freq'] = 'DAILY'; $result['interval'] /= 1440; if ($arp) { $result['byhour'] = floor($arp->StartTimeOffset / 60); $result['byminute'] = $arp->StartTimeOffset - $result['byhour'] * 60; } break; case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_WEEK: $result['freq'] = 'WEEKLY'; $result['byday'] = self::bitmask2day($rp->PatternTypeSpecific); if ($rp->Period >= 1) { $result['wkst'] = self::bitmask2day($rp->FirstDOW); } break; default: // monthly/yearly $evenly_divisible = $rp->Period % 12 == 0; switch ($rp->PatternType) { case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTH: case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHEND: $result['freq'] = $evenly_divisible ? 'YEARLY' : 'MONTHLY'; break; case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH: case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_HJMONTHNTH: $result['freq'] = $evenly_divisible ? 'YEARLY-NTH' : 'MONTHLY-NTH'; break; default: // not-supported return; } if ($result['freq'] = 'MONTHLY') { $rule['bymonthday'] = intval($rp->PatternTypeSpecific == 0x0000001F ? -1 : $rp->PatternTypeSpecific); } else if ($result['freq'] = 'MONTHLY-NTH') { $result['freq'] = 'MONTHLY'; $result['byday'] = self::bitmask2day($rp->PatternTypeSpecific[0]); if ($rp->PatternTypeSpecific[1]) { $result['bysetpos'] = intval($rp->PatternTypeSpecific[1] == 0x00000005 ? -1 : $rp->PatternTypeSpecific[1]); } } else if ($result['freq'] = 'YEARLY') { $result['interval'] /= 12; $rule['bymonthday'] = intval($rp->PatternTypeSpecific == 0x0000001F ? -1 : $rp->PatternTypeSpecific); $rule['bymonth'] = 0;// @TODO: month from FirstDateTime } else if ($result['freq'] = 'YEARLY-NTH') { $result['freq'] = 'YEARLY'; $result['interval'] /= 12; $result['byday'] = self::bitmask2day($rp->PatternTypeSpecific[0]); $result['bymonth'] = 0;// @TODO: month from FirstDateTime if ($rp->PatternTypeSpecific[1]) { $result['bysetpos'] = intval($rp->PatternTypeSpecific[1] == 0x00000005 ? -1 : $rp->PatternTypeSpecific[1]); } } } if ($rp->EndType == kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_AFTER) { // @TODO: set UNTIL to EndDate + StartTimeOffset, or the midnight of EndDate } else if ($rp->EndType == kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC) { $result['count'] = $rp->OccurrenceCount; } if ($result['interval'] == 1) { unset($result['interval']); } $object['rrule']['recur'] = $result; $object['exdate'] = array(); $object['rdate'] = array(); // $exception_info = (array) $rp->ExceptionInfo; // $extended_exception = (array) $rp->ExtendedException; $modified_dates = (array) $rp->ModifiedInstanceDates; $deleted_dates = (array) $rp->DeletedInstanceDates; // Deleted/Modified exceptions (EXDATE/RDATE) foreach ($deleted_dates as $date) { $idx = in_array($date, $modified_dates) ? 'rdate' : 'exdate'; $dt = self::date_minutes2php($date); if ($dt) { $object[$idx]['date'][] = $dt->format('Y-m-d'); } } } + /** + * Convert Kolab description property into MAPI body properties + * + * @param array $data Kolab object + * @param array $result Object data (MAPI format) + * @param string $field Kolab object property name + * @param string $force Force output format (plain|html) + */ + protected function body_from_kolab($data, &$result, $field = 'description', $force = null) + { + $text = $data[$field]; + + if (self::is_html($text)) { + // some objects does not support HTML e.g. notes + if ($force == 'plain') { + $h2t = new rcube_html2text($text, false, false, 0); + $result['PidTagBody'] = trim($h2t->get_text()); + } + else { + $result['PidTagHtml'] = $text; + } + } + else if ($text) { + $result['PidTagBody'] = $text; + } + } + + /** + * Convert MAPI body properties into Kolab + * + * @param string $data MAPI object + * @param array $result Kolab object + * @param string $field Kolab object property name + * @param string $force Force output format (plain|html) + */ + protected function body_to_kolab($data, &$result, $field = 'description', $force = null) + { + // Kolab supports HTML and plain text but not RTF + + if (array_key_exists('PidTagRtfCompressed', $data)) { + require_once 'rtf.php'; + + $rtf = new rtf(base64_decode($data['PidTagRtfCompressed'])); + $text = $rtf->parse($force ?: 'html'); + } + else if (array_key_exists('PidTagHtml', $data)) { + $text = $data['PidTagHtml']; + // some objects does not support HTML e.g. contacts + if ($force == 'plain') { + $h2t = new rcube_html2text($text, false, false, 0); + $text = trim($h2t->get_text()); + } + } + else if (array_key_exists('PidTagBody', $data)) { + $text = $data['PidTagBody']; + } + + if (isset($text)) { + $result[$field] = $text; + } + } + /** * Returns number of minutes between midnight 1601-01-01 * and specified UTC DateTime */ public static function date_php2minutes($date) { $start = new DateTime('1601-01-01 00:00:00 UTC'); // make sure the specified date is in UTC $date->setTimezone(new DateTimeZone('UTC')); return (int) round(($date->getTimestamp() - $start->getTimestamp()) / 60); } /** * Convert number of minutes between midnight 1601-01-01 (UTC) into PHP DateTime * * @return DateTime|bool DateTime object or False on failure */ public static function date_minutes2php($minutes) { $datetime = new DateTime('1601-01-01 00:00:00 UTC'); $interval = new DateInterval(sprintf('PT%dM', $minutes)); return $datetime->add($interval); } /** * Convert DateTime object to MAPI date format */ public static function date_php2mapi($date, $utc = true, $time = null) { // convert string to DateTime if (!is_object($date) && !empty($date)) { // convert date to datetime on 00:00:00 if (preg_match('/^([0-9]{4})-?([0-9]{2})-?([0-9]{2})$/', $date, $m)) { $date = $m[1] . '-' . $m[2] . '-' . $m[3] . 'T00:00:00+00:00'; } $date = new DateTime($date); } else if (is_object($date) && $utc) { // clone the date object if we're going to change timezone $date = clone $date; } else { return; } if ($utc) { $date->setTimezone(new DateTimeZone('UTC')); } if (!empty($time)) { $date->setTime((int) $time['hour'], (int) $time['minute'], (int) $time['second']); } // MAPI PTypTime is 64-bit integer representing the number // of 100-nanosecond intervals since January 1, 1601. // Mapistore format for this type is a float number // seconds since 1601-01-01 00:00:00 $seconds = floatval($date->format('U')) + 11644473600; /* if ($microseconds = intval($date->format('u'))) { $seconds += $microseconds/1000000; } */ return $seconds; } /** * Convert date-time from MAPI format to DateTime */ public static function date_mapi2php($date) { $seconds = floatval(sprintf('%.0f', $date)); // assumes we're working with dates after 1970-01-01 $dt = new DateTime('@' . intval($seconds - 11644473600), new DateTimeZone('UTC')); /* if ($microseconds = intval(($date - $seconds) * 1000000)) { $dt = new DateTime($dt->format('Y-m-d H:i:s') . '.' . $microseconds, $dt->getTimezone()); } */ return $dt; } /** * Setting PidTagRecipientType according to [MS-OXCICAL 2.1.3.1.1.20.2] */ protected function to_recipient_type($cutype, $role) { if ($cutype && in_array($cutype, array('RESOURCE', 'ROOM'))) { return 0x00000003; } if ($role && ($type = $this->recipient_type_map[$role])) { return $type; } return 0x00000001; } /** * Converts string of days (TU,TH) to bitmask used by MAPI * * @param string $days * * @return int */ protected static function day2bitmask($days, $prefix = '') { $days = explode(',', $days); $result = 0; foreach ($days as $day) { $result = $result + self::$recurrence_day_map[$prefix.$day]; } return $result; } /** * Convert bitmask used by MAPI to string of days (TU,TH) * * @param int $days * * @return string */ protected static function bitmask2day($days) { $days_arr = array(); foreach (self::$recurrence_day_map as $day => $bit) { if (($days & $bit) === $bit) { $days_arr[] = preg_replace('/^BYDAY-/', '', $day); } } $result = implode(',', $days_arr); return $result; } + + /** + * Determine whether the given event description is HTML formatted + */ + protected static function is_html($text) + { + // check for opening and closing or tags + return preg_match('/<(html|body)(\s+[a-z]|>)/', $text, $m) && strpos($text, '') > 0; + } } diff --git a/lib/filter/mapistore/contact.php b/lib/filter/mapistore/contact.php index e0536ca..91fc0cf 100644 --- a/lib/filter/mapistore/contact.php +++ b/lib/filter/mapistore/contact.php @@ -1,688 +1,696 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_contact extends kolab_api_filter_mapistore_common { const PHOTO_ATTACHMENT_ID = 9999; protected $model = 'contact'; protected $map = array( // contact name properties [MS-OXOCNTC] 'PidTagNickname' => 'nickname', // PtypString 'PidTagGeneration' => 'n.suffix', // PtypString 'PidTagDisplayNamePrefix' => 'n.prefix', // PtypString 'PidTagSurname' => 'n.surname', // PtypString 'PidTagMiddleName' => 'n.additional', // PtypString 'PidTagGivenName' => 'n.given', // PtypString 'PidTagInitials' => 'x-custom.MAPI:PidTagInitials', // PtypString 'PidTagDisplayName' => 'fn', // PtypString 'PidLidYomiFirstName' => '', // PtypString 'PidLidYomiLastName' => '', // PtypString 'PidLidFileUnder' => '', // PtypString 'PidLidFileUnderId' => '', // PtypInteger32 'PidLidFileUnderList' => '', // PtypMultipleInteger32 // electronic and phisical address properties 'PidTagPrimaryFaxNumber' => 'x-custom.MAPI:PidTagPrimaryFaxNumber', // PtypString 'PidTagBusinessFaxNumber' => 'x-custom.MAPI:PidTagBusinessFaxNumber', // PtypString 'PidTagHomeFaxNumber' => '', // PtypString 'PidTagHomeAddressStreet' => '', // PtypString 'PidTagHomeAddressCity' => '', // PtypString 'PidTagHomeAddressStateOrProvince' => '', // PtypString 'PidTagHomeAddressPostalCode' => '', // PtypString 'PidTagHomeAddressCountry' => '', // PtypString 'PidLidHomeAddressCountryCode' => '', // PtypString 'PidTagHomeAddressPostOfficeBox' => '', // PtypString 'PidLidHomeAddress' => '', // @TODO: ? 'PidLidWorkAddressStreet' => '', // PtypString 'PidLidWorkAddressCity' => '', // PtypString 'PidLidWorkAddressState' => '', // PtypString 'PidLidWorkAddressPostalCode' => '', // PtypString 'PidLidWorkAddressCountry' => '', // PtypString 'PidLidWorkAddressCountryCode' => '', // PtypString 'PidLidWorkAddressPostOfficeBox' => '', // PtypString 'PidLidWorkAddress' => '', // @TODO: ? 'PidTagOtherAddressStreet' => '', // PtypString 'PidTagOtherAddressCity' => '', // PtypString 'PidTagOtherAddressStateOrProvince' => '', // PtypString 'PidTagOtherAddressPostalCode' => '', // PtypString 'PidTagOtherAddressCountry' => '', // PtypString 'PidLidOtherAddressCountryCode' => '', // PtypString 'PidTagOtherAddressPostOfficeBox' => '', // PtypString 'PidLidOtherAddress' => '', // @TODO: ? // PtypString 'PidTagStreetAddress' => '', // @TODO: ? // PtypString 'PidTagLocality' => '', // @TODO: ? // PtypString 'PidTagStateOrProvince' => '', // @TODO: ? // PtypString 'PidTagPostalCode' => '', // @TODO: ? // PtypString 'PidTagCountry' => '', // @TODO: ? // PtypString 'PidLidAddressCountryCode' => '', // @TODO: ? // PtypString 'PidTagPostOfficeBox' => '', // @TODO: ? // PtypString 'PidTagPostalAddress' => '', // @TODO: ? // PtypString 'PidLidPostalAddressId' => '', // PtypInteger32 'PidTagPagerTelephoneNumber' => '', // PtypString 'PidTagCallbackTelephoneNumber' => '', // PtypString 'PidTagBusinessTelephoneNumber' => '', // PtypString 'PidTagHomeTelephoneNumber' => '', // PtypString 'PidTagPrimaryTelephoneNumber' => '', // PtypString 'PidTagBusiness2TelephoneNumber' => '', // PtypString 'PidTagMobileTelephoneNumber' => '', // PtypString 'PidTagRadioTelephoneNumber' => '', // PtypString 'PidTagCarTelephoneNumber' => '', // PtypString 'PidTagOtherTelephoneNumber' => '', // PtypString 'PidTagAssistantTelephoneNumber' => '', // PtypString 'PidTagHome2TelephoneNumber' => 'x-custom.MAPI:PidTagHome2TelephoneNumber', 'PidTagTelecommunicationsDeviceForDeafTelephoneNumber' => 'x-custom.MAPI:PidTagTelecommunicationsDeviceForDeafTelephoneNumber', 'PidTagCompanyMainTelephoneNumber' => 'x-custom.MAPI:PidTagCompanyMainTelephoneNumber', 'PidTagTelexNumber' => '', // PtypString 'PidTagIsdnNumber' => '', // PtypString 'PidLidAddressBookProviderEmailList' => '', // PtypMultipleInteger32, @TODO: ? 'PidLidAddressBookProviderArrayType' => '', // PtypInteger32, @TODO: ? // event properties 'PidTagBirthday' => 'bday', // PtypTime, UTC 'PidLidBirthdayLocal' => '', // PtypTime, @TODO 'PidLidBirthdayEventEntryId' => '', // PtypBinary 'PidTagWeddingAnniversary' => 'anniversary', // PtypTime, UTC 'PidLidWeddingAnniversaryLocal' => '', // PtypTime, @TODO 'PidLidAnniversaryEventEntryId' => '', // PtypBinary // professional properties 'PidTagTitle' => 'title', // PtypString 'PidTagCompanyName' => '', // PtypString 'PidLidYomiCompanyName' => '', // PtypString 'PidTagDepartmentName' => '', // PtypString 'PidTagOfficeLocation' => 'x-custom.MAPI:PidTagOfficeLocation', // PtypString 'PidTagManagerName' => '', // PtypString 'PidTagAssistant' => '', // PtypString 'PidTagProfession' => 'group.role', // PtypString 'PidLidHasPicture' => '', // PtypBoolean, more about photo attachments in MS-OXOCNTC // other properties 'PidTagHobbies' => 'x-custom.MAPI:PidTagHobbies', // PtypString 'PidTagSpouseName' => '', // PtypString 'PidTagLanguage' => 'lang', // PtypString 'PidTagLocation' => 'x-custom.MAPI:PidTagLocation', // PtypString 'PidLidInstantMessagingAddress' => 'impp', // PtypString 'PidTagOrganizationalIdNumber' => 'x-custom.MAPI:PidTagOrganizationalIdNumber',// PtypString 'PidTagCustomerId' => 'x-custom.MAPI:PidTagCustomerId', // PtypString 'PidTagGovernmentIdNumber' => 'x-custom.MAPI:PidTagGovernmentIdNumber',// PtypString 'PidTagPersonalHomePage' => 'url', // PtypString 'PidTagBusinessHomePage' => 'x-custom.MAPI:PidTagBussinessHomePage', // PtypString 'PidTagFtpSite' => 'x-custom.MAPI:PidTagFtpSite', // PtypString 'PidTagReferredByName' => 'x-custom.MAPI:PidTagReferredByName', // PtypString 'PidLidBilling' => 'x-custom.MAPI:PidLidBilling', // PtypString 'PidLidFreeBusyLocation' => 'fburl', // PtypString 'PidTagChildrenNames' => '', // PtypMultipleString 'PidTagGender' => 'gender', // PtypString 'PidTagUserX509Certificate' => 'key', // PtypMultipleBinary 'PidTagMessageClass' => '', // PtypString: IPM.Contact, IPM.DistList - 'PidTagBody' => 'note', // PtypString + 'PidTagBody' => '', // 'note' // PtypString // contact aggregation properties - skipped 'PidTagLastModificationTime' => 'rev', // PtypTime // distribution lists [MS-OXOCNTC] 'PidLidDistributionListName' => '', // PtypString = PidTagDisplayName 'PidLidDistributionListMembers' => '', // PtypMultipleBinary 'PidLidDistributionListOneOffMembers' => '', // PtypMultipleBinary 'PidLidDistributionListChecksum' => '', // PtypInteger32 'PidLidDistributionListStream' => '', // PtypBinary ); protected $gender_map = array( 0 => '', 1 => 'F', 2 => 'M', ); protected $phone_map = array( 'PidTagPagerTelephoneNumber' => 'pager', 'PidTagBusinessTelephoneNumber' => 'work', 'PidTagHomeTelephoneNumber' => 'home', 'PidTagMobileTelephoneNumber' => 'cell', 'PidTagCarTelephoneNumber' => 'x-car', 'PidTagOtherTelephoneNumber' => 'textphone', 'PidTagBusinessFaxNumber' => 'faxwork', 'PidTagHomeFaxNumber' => 'faxhome', ); protected $email_map = array( 'PidLidEmail1EmailAddress' => 'home', 'PidLidEmail2EmailAddress' => 'work', 'PidLidEmail3EmailAddress' => 'other', ); protected $address_ids = array( 'home' => 0x00000001, 'work' => 0x00000002, 'other' => 0x00000003, ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { $result = array( 'PidTagMessageClass' => $data['kind'] == 'group' ? 'IPM.DistList' : 'IPM.Contact', // mapistore REST API specific properties 'collection' => 'contacts', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidTagGender': $value = (int) array_search($value, $this->gender_map); break; case 'PidTagPersonalHomePage': case 'PidLidInstantMessagingAddress': if (is_array($value)) { $value = $value[0]; } break; case 'PidTagBirthday': case 'PidTagWeddingAnniversary': case 'PidTagLastModificationTime': $value = $this->date_php2mapi($value, false); break; case 'PidTagUserX509Certificate': foreach ((array) $value as $val) { if ($val && preg_match('|^data:application/pkcs7-mime;base64,|i', $val, $m)) { $result[$mapi_idx] = substr($val, strlen($m[0])); continue 3; } } $value = null; break; case 'PidTagTitle': if (is_array($value)) { $value = $value[0]; } break; } if ($value === null) { continue; } $result[$mapi_idx] = $value; } + // notes can be in plain text format only? + $this->body_from_kolab($data, $result, 'note'); + // contact photo attachment [MS-OXVCARD 2.1.3.2.4] if (!empty($data['photo'])) { // @TODO: check if photo is one of .bmp, .gif, .jpeg, .png // Photo in MAPI is handled as attachment // Set PidTagAttachmentContactPhoto=true on attachment object $result['PidLidHasPicture'] = true; // @FIXME: should we set PidTagHasAttachments? } // Organization/Department $organization = $data['group']['org']; if (is_array($organization)) { $result['PidTagCompanyName'] = $organization[0]; $result['PidTagDepartmentName'] = $organization[1]; } else if ($organization !== null) { $result['PidTagCompanyName'] = $organization; } // Manager/Assistant $related = $data['group']['related']; if ($related && $related['parameters']) { $related = array($related); } foreach ((array) $related as $rel) { $type = $rel['parameters']['type']; if ($type == 'x-manager') { $result['PidTagManagerName'] = $rel['text']; } else if ($type == 'x-assistant') { $result['PidTagAssistant'] = $rel['text']; } } // Children, Spouse foreach ((array) $data['related'] as $rel) { $type = $rel['parameters']['type']; if ($type == 'child') { $result['PidTagChildrensNames'][] = $rel['text']; } else if ($type == 'spouse') { $result['PidTagSpouseName'] = $rel['text']; } } // Emails $email_map = array_flip($this->email_map); foreach ((array) $data['email'] as $email) { $type = is_array($email) ? $email['parameters']['type'] : 'other'; $key = $email_map[$type] ?: $email_map['other']; // @TODO: This may be addr-spec (RFC5322), we should parse it // and fill also *AddressType and *DisplayName $result[$key] = is_array($email) ? $email['text'] : $email; } // Phone(s) $phone_map = array_flip($this->phone_map); $phones = $data['tel']; if ($phones && $phones['parameters']) { $phones = array($phones); } foreach ((array) $phones as $phone) { $type = implode('', (array)$phone['parameters']['type']); if ($phone['text'] && ($idx = $phone_map[$type])) { $result[$idx] = $phone['text']; } } // Addresses(s) $addresses = $data['adr']; if ($addresses && $addresses['parameters']) { $addresses = array($addresses); } foreach ((array) $addresses as $addr) { $type = $addr['parameters']['type']; $pref = $addr['parameters']['pref']; $address = null; if ($type == 'home') { $address = array( 'PidTagHomeAddressStreet' => $addr['street'], 'PidTagHomeAddressCity' => $addr['locality'], 'PidTagHomeAddressStateOrProvince' => $addr['region'], 'PidTagHomeAddressPostalCode' => $addr['code'], 'PidTagHomeAddressCountry' => $addr['country'], 'PidTagHomeAddressPostOfficeBox' => $addr['pobox'], ); } else if ($type == 'work') { $address = array( 'PidLidWorkAddressStreet' => $addr['street'], 'PidLidWorkAddressCity' => $addr['locality'], 'PidLidWorkAddressState' => $addr['region'], 'PidLidWorkAddressPostalCode' => $addr['code'], 'PidLidWorkAddressCountry' => $addr['country'], 'PidLidWorkAddressPostOfficeBox' => $addr['pobox'], ); } if (!empty($address)) { $result = array_merge($result, array_filter($address)); if (!empty($pref)) { $result['PidLidPostalAddressId'] = $this->address_ids[$type]; } } } $other_adr_map = array( 'street' => 'PidTagOtherAddressStreet', 'locality' => 'PidTagOtherAddressCity', 'region' => 'PidTagOtherAddressStateOrProvince', 'code' => 'PidTagOtherAddressPostalCode', 'country' => 'PidTagOtherAddressCountry', 'pobox' => 'PidTagOtherAddressPostOfficeBox', ); foreach ((array) $data['group']['adr'] as $idx => $value) { if ($value && ($key = $other_adr_map[$idx])) { $result[$key] = $value; } } // Group members $this->members_from_kolab($data, $result); $this->parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagBirthday': case 'PidTagWeddingAnniversary': if ($value) { $value = $this->date_mapi2php($value); $value = $value->format('Y-m-d'); } break; case 'PidTagLastModificationTime': if ($value) { $value = $this->date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; case 'PidTagGender': $value = $this->gender_map[(int)$value]; break; case 'PidTagUserX509Certificate': if (!empty($value)) { $value = array('data:application/pkcs7-mime;base64,' . $value); } break; case 'PidTagPersonalHomePage': case 'PidLidInstantMessagingAddress': if (!empty($value)) { $value = array($value); } break; } $this->set_kolab_value($result, $kolab_idx, $value); } + // notes + $this->body_to_kolab($data, $result, 'note', 'plain'); + if (!empty($data['PidTagMessageClass'])) { $result['kind'] = stripos($data['PidTagMessageClass'], 'IPM.DistList') === 0 ? 'group' : 'individual'; } // MS-OXVCARD 2.1.3.2.1 if (!empty($data['PidTagNormalizedSubject']) && empty($data['PidTagDisplayName'])) { $result['fn'] = $data['PidTagNormalizedSubject']; } // Organization/Department if ($data['PidTagCompanyName']) { $result['group']['org'][] = $data['PidTagCompanyName']; } if (!empty($data['PidTagDepartmentName'])) { $result['group']['org'][] = $data['PidTagDepartmentName']; } // Manager if ($data['PidTagManagerName']) { $result['group']['related'][] = array( 'parameters' => array('type' => 'x-manager'), 'text' => $data['PidTagManagerName'], ); } // Assistant if ($data['PidTagAssistant']) { $result['group']['related'][] = array( 'parameters' => array('type' => 'x-assistant'), 'text' => $data['PidTagAssistant'], ); } // Spouse if ($data['PidTagSpouseName']) { $result['related'][] = array( 'parameters' => array('type' => 'spouse'), 'text' => $data['PidTagSpouseName'], ); } // Children foreach ((array) $data['PidTagChildrensNames'] as $child) { $result['related'][] = array( 'parameters' => array('type' => 'child'), 'text' => $child, ); } // Emails foreach ($this->email_map as $mapi_idx => $type) { if ($email = $data[$mapi_idx]) { $result['email'][] = array( 'parameters' => array('type' => $type), 'text' => $email, ); } } // Phone(s) foreach ($this->phone_map as $mapi_idx => $type) { if (array_key_exists($mapi_idx, $data)) { // first remove the old phone... if (!empty($object['tel'])) { foreach ($object['tel'] as $idx => $phone) { $pt = implode('', (array) $phone['parameters']['type']); if ($pt == $type) { unset($object['tel'][$idx]); } } } if ($tel = $data[$mapi_idx]) { if (preg_match('/^fax(work|home)$/', $type, $m)) { $type = array('fax', $m[1]); } // and add it to the list $result['tel'][] = array( 'parameters' => array('type' => $type), 'text' => $tel, ); } } } if (!empty($object['tel'])) { $result['tel'] = array_merge((array) $result['tel'], (array) $object['tel']); } // Preferred (mailing) address if ($data['PidLidPostalAddressId']) { $map = array_flip($this->address_ids); $pref = $map[$data['PidLidPostalAddressId']]; } // Home address $address = array(); $adr_map = array( 'PidTagHomeAddressStreet' => 'street', 'PidTagHomeAddressCity' => 'locality', 'PidTagHomeAddressStateOrProvince' => 'region', 'PidTagHomeAddressPostalCode' => 'code', 'PidTagHomeAddressCountry' => 'country', 'PidTagHomeAddressPostOfficeBox' => 'pobox', ); foreach ($adr_map as $mapi_idx => $idx) { if ($adr = $data[$mapi_idx]) { $address[$idx] = $adr; } } if (!empty($address)) { $type = array('parameters' => array('type' => 'home')); if ($pref == 'home') { $type['parameters']['pref'] = 1; } $result['adr'][] = array_merge($address, $type); } // Work address $address = array(); $adr_map = array( 'PidLidWorkAddressStreet' => 'street', 'PidLidWorkAddressCity' => 'locality', 'PidLidWorkAddressState' => 'region', 'PidLidWorkAddressPostalCode' => 'code', 'PidLidWorkAddressCountry' => 'country', 'PidLidWorkAddressPostOfficeBox' => 'pobox', ); foreach ($adr_map as $mapi_idx => $idx) { if ($adr = $data[$mapi_idx]) { $address[$idx] = $adr; } } if (!empty($address)) { $type = array('parameters' => array('type' => 'work')); if ($pref == 'work') { $type['parameters']['pref'] = 1; } $result['adr'][] = array_merge($address, $type); } // Office address $address = array(); $adr_map = array( 'PidTagOtherAddressStreet' => 'street', 'PidTagOtherAddressCity' => 'locality', 'PidTagOtherAddressStateOrProvince' => 'region', 'PidTagOtherAddressPostalCode' => 'code', 'PidTagOtherAddressCountry' => 'country', 'PidTagOtherAddressPostOfficeBox' => 'pobox', ); foreach ($adr_map as $mapi_idx => $idx) { if ($adr = $data[$mapi_idx]) { $address[$idx] = $adr; } } if (!empty($address)) { $type = array(); if ($pref == 'other') { $type['parameters']['pref'] = 1; } $result['group']['adr'] = array_merge($address, $type); } // Group members $this->members_to_kolab($data, $result); $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); + $map['PidTagBody'] = 'note'; + return $map; } /** * Return attachment data for photo of the contact * * @param array $contact Contact data * * @return array Attachment data */ public static function photo_attachment($contact) { if (!empty($contact['photo'])) { $bin_data = $contact['photo']; $mimetype = rcube_mime::file_content_type($bin_data, 'ContactPicture.jpg', 'image/jpeg', true); list(, $type) = explode('/', $mimetype); $ext = $type == 'jpeg' ? 'jpg' : $type; // @TODO: Do we need to convert to JPEG? // [MS-OXOCNTC]: The value of the PidTagAttachDataBinary property, // which is the contents of the attachment, SHOULD be in JPEG format. // Support for other formats is as determined by the implementer. $attachment = array( 'is_photo' => true, 'filename' => 'ContactPicture.' . $ext, 'size' => strlen($bin_data), 'content' => $bin_data, 'mimetype' => $mimetype, 'id' => self::PHOTO_ATTACHMENT_ID, ); if ($attachment['size']) { return $attachment; } } } /** * Convert Kolab members list into MAPI properties */ protected function members_from_kolab($data, &$result) { // @TODO foreach ((array) $data['member'] as $member) { } } /** * Convert MAPI properties into Kolab member array */ protected function members_to_kolab($data, &$result) { // @TODO } } diff --git a/lib/filter/mapistore/event.php b/lib/filter/mapistore/event.php index d53af78..2f8903e 100644 --- a/lib/filter/mapistore/event.php +++ b/lib/filter/mapistore/event.php @@ -1,459 +1,467 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_event extends kolab_api_filter_mapistore_common { protected $model = 'event'; protected $map = array( // common properties [MS-OXOCAL] 'PidLidAppointmentSequence' => 'sequence', // PtypInteger32 'PidLidBusyStatus' => '', // PtypInteger32, @TODO: X-MICROSOFT-CDO-BUSYSTATUS 'PidLidAppointmentAuxiliaryFlags' => '', // PtypInteger32 'PidLidLocation' => 'location', // PtypString 'PidLidAppointmentStartWhole' => 'dtstart', // PtypTime, UTC 'PidLidAppointmentEndWhole' => 'dtend', // PtypTime, UTC 'PidLidAppointmentDuration' => '', // PtypInteger32, optional 'PidLidAppointmentSubType' => '', // PtypBoolean 'PidLidAppointmentStateFlags' => '', // PtypInteger32 'PidLidResponseStatus' => '', // PtypInteger32 'PidLidRecurring' => '', // PtypBoolean 'PidLidIsRecurring' => '', // PtypBoolean 'PidLidClipStart' => '', // PtypTime 'PidLidClipEnd' => '', // PtypTime 'PidLidAllAttendeesString' => '', // PtypString 'PidLidToAttendeesString' => '', // PtypString 'PidLidCCAttendeesString' => '', // PtypString 'PidLidNonSendableTo' => '', // PtypString 'PidLidNonSendableCc' => '', // PtypString 'PidLidNonSendableBcc' => '', // PtypString 'PidLidNonSendToTrackStatus' => '', // PtypMultipleInteger32 'PidLidNonSendCcTrackStatus' => '', // PtypMultipleInteger32 'PidLidNonSendBccTrackStatus' => '', // PtypMultipleInteger32 'PidLidAppointmentUnsendableRecipients' => '', // PtypBinary, optional 'PidLidAppointmentNotAllowPropose' => '', // PtypBoolean, @TODO: X-MICROSOFT-CDO-DISALLOW-COUNTER 'PidLidGlobalObjectId' => '', // PtypBinary 'PidLidCleanGlobalObjectId' => '', // PtypBinary 'PidTagOwnerAppointmentId' => '', // PtypInteger32, @TODO: X-MICROSOFT-CDO-OWNERAPPTID 'PidTagStartDate' => '', // PtypTime 'PidTagEndDate' => '', // PtypTime 'PidLidCommonStart' => '', // PtypTime 'PidLidCommonEnd' => '', // PtypTime 'PidLidOwnerCriticalChange' => '', // PtypTime, @TODO: X-MICROSOFT-CDO-CRITICAL-CHANGE 'PidLidIsException' => '', // PtypBoolean 'PidTagResponseRequested' => '', // PtypBoolean 'PidTagReplyRequested' => '', // PtypBoolean 'PidLidTimeZoneStruct' => '', // PtypBinary 'PidLidTimeZoneDescription' => '', // PtypString 'PidLidAppointmentTimeZoneDefinitionRecur' => '', // PtypBinary 'PidLidAppointmentTimeZoneDefinitionStartDisplay' => '', // PtypBinary 'PidLidAppointmentTimeZoneDefinitionEndDisplay' => '', // PtypBinary 'PidLidAppointmentRecur' => '', // PtypBinary 'PidLidRecurrenceType' => '', // PtypInteger32 'PidLidRecurrencePattern' => '', // PtypString 'PidLidLinkedTaskItems' => '', // PtypMultipleBinary 'PidLidMeetingWorkspaceUrl' => '', // PtypString 'PidTagIconIndex' => '', // PtypInteger32 'PidLidAppointmentColor' => '', // PtypInteger32 'PidLidAppointmentReplyTime' => '', // @TODO: X-MICROSOFT-CDO-REPLYTIME 'PidLidIntendedBusyStatus' => '', // @TODO: X-MICROSOFT-CDO-INTENDEDSTATUS // calendar object properties [MS-OXOCAL] 'PidTagMessageClass' => '', 'PidLidSideEffects' => '', // PtypInteger32 'PidLidFExceptionAttendees' => '', // PtypBoolean 'PidLidClientIntent' => '', // PtypInteger32 // common props [MS-OXCMSG] 'PidTagSubject' => 'summary', - 'PidTagBody' => 'description', - 'PidTagHtml' => '', // @TODO: (?) + 'PidTagBody' => '', + 'PidTagHtml' => '', 'PidTagNativeBody' => '', 'PidTagBodyHtml' => '', 'PidTagRtfCompressed' => '', 'PidTagInternetCodepage' => '', 'PidTagContentId' => '', 'PidTagBodyContentLocation' => '', 'PidTagImportance' => 'priority', 'PidTagSensitivity' => 'class', 'PidLidPrivate' => '', 'PidTagCreationTime' => 'created', 'PidTagLastModificationTime' => 'dtstamp', // reminder properties [MS-OXORMDR] 'PidLidReminderSet' => '', // PtypBoolean 'PidLidReminderSignalTime' => '', // PtypTime 'PidLidReminderDelta' => '', // PtypInteger32 'PidLidReminderTime' => '', // PtypTime 'PidLidReminderOverride' => '', // PtypBoolean 'PidLidReminderPlaySound' => '', // PtypBoolean 'PidLidReminderFileParameter' => '', // PtypString 'PidTagReplyTime' => '', // PtypTime 'PidLidReminderType' => '', // PtypInteger32 ); /** * Message importance for PidTagImportance as defined in [MS-OXCMSG] */ protected $importance = array( 0 => 0x00000000, 1 => 0x00000002, 2 => 0x00000002, 3 => 0x00000002, 4 => 0x00000002, 5 => 0x00000001, 6 => 0x00000000, 7 => 0x00000000, 8 => 0x00000000, 9 => 0x00000000, ); /** * Message sesnitivity for PidTagSensitivity as defined in [MS-OXCMSG] */ protected $sensitivity = array( 'public' => 0x00000000, 'personal' => 0x00000001, 'private' => 0x00000002, 'confidential' => 0x00000003, ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { $result = array( 'PidTagMessageClass' => 'IPM.Appointment', // mapistore REST API specific properties 'collection' => 'calendars', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidTagSensitivity': $value = (int) $this->sensitivity[strtolower($value)]; break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': $value = $this->date_php2mapi($value, true); break; case 'PidTagImportance': $value = (int) $this->importance[(int) $value]; break; case 'PidLidAppointmentStartWhole': case 'PidLidAppointmentEndWhole': $dt = kolab_api_input_json::to_datetime($value); $value = $this->date_php2mapi($dt, true); // this is all-day event if ($dt->_dateonly) { $result['PidLidAppointmentSubType'] = 0x00000001; } else if (empty($data['rrule']) && $dt->getTimezone()->getName() != 'UTC') { $idx = sprintf('PidLidAppointmentTimeZoneDefinition%sDisplay', strpos($mapi_idx, 'Start') ? 'Start' : 'End'); $result[$idx] = $this->timezone_definition($dt); } break; + } $result[$mapi_idx] = $value; } + // event description + $this->body_from_kolab($data, $result); + // fix end dat of all-day event if ($result['PidLidAppointmentSubType'] && $result['PidLidAppointmentStartWhole'] && $result['PidLidAppointmentStartWhole'] == $result['PidLidAppointmentEndWhole'] ) { $result['PidLidAppointmentEndWhole'] += 24 * 60 * 60; } // Organizer if (!empty($data['organizer'])) { $this->attendee_to_recipient($data['organizer'], $result, true); } // Attendees [MS-OXCICAL 2.1.3.1.1.20.2] foreach ((array) $data['attendee'] as $attendee) { $this->attendee_to_recipient($attendee, $result); } // PidLidAppointmentDuration if ($result['PidLidAppointmentStartWhole'] && $result['PidLidAppointmentEndWhole']) { $result['PidLidAppointmentDuration'] = (int) round(($result['PidLidAppointmentEndWhole'] - $result['PidLidAppointmentStartWhole']) / 60); } else if ($result['PidLidAppointmentStartWhole'] && $data['duration']) { try { $interval = new DateInterval($data['duration']); $duration = min(24 * 60, $interval->i + $interval->h * 60 + $interval->d * 24 * 60); $result['PidLidAppointmentDuration'] = $duration; $result['PidLidAppointmentEndWhole'] = $result['PidLidAppointmentStartWhole'] + $duration * 60; } catch (Exception $e) { rcube::raise_error(array( 'line' => __LINE__, 'file' => __FILE__, 'message' => $e->getMessage(), ), true, false); } } // @TODO: exceptions, resources // Recurrence if ($rule = $this->recurrence_from_kolab($data, $result)) { $result['PidLidAppointmentRecur'] = $rule; if ($dt && $dt->getTimezone()->getName() != 'UTC') { $result['PidLidTimeZoneStruct'] = $this->timezone_structure($dt); $result['PidLidTimeZoneDescription'] = $this->timezone_description($dt); } } // Alarms (MAPI supports only one) $this->alarm_from_kolab($data, $result); $this->parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); if ($data['PidLidTimeZoneStruct']) { $timezone = $this->timezone_structure_to_tzname($data['PidLidTimeZoneStruct']); } foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagImportance': $map = array( 0x00000002 => 1, 0x00000001 => 5, 0x00000000 => 9, ); $value = (int) $map[(int) $value]; break; case 'PidTagSensitivity': $map = array_flip($this->sensitivity); $value = $map[$value]; break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $value = $this->date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; case 'PidLidAppointmentStartWhole': case 'PidLidAppointmentEndWhole': if ($value) { $datetime = $this->date_mapi2php($value); $datetime->_dateonly = !empty($data['PidLidAppointmentSubType']); $tz_idx = sprintf('PidLidAppointmentTimeZoneDefinition%sDisplay', strpos($mapi_idx, 'Start') ? 'Start' : 'End'); if ($data[$tz_idx]) { $tz = $this->timezone_definition_to_tzname($data[$tz_idx]); } else { $tz = $timezone; } $value = kolab_api_input_json::from_datetime($datetime, $tz); } break; } $result[$kolab_idx] = $value; } + // event description + $this->body_to_kolab($data, $result); + // Recurrence if (array_key_exists('PidLidAppointmentRecur', $data)) { $this->recurrence_to_kolab($data['PidLidAppointmentRecur'], $result); } // Alarms (MAPI supports only one, DISPLAY) $this->alarm_to_kolab($data, $result); if (array_key_exists('recipients', $data)) { $result['attendee'] = array(); $result['organizer'] = array(); foreach ((array) $data['recipients'] as $recipient) { $this->recipient_to_attendee($recipient, $result); } } // @TODO: exception, resources $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); // @TODO: add properties that are not in the map $map['PidLidAppointmentRecur'] = 'rrule'; + $map['PidTagBody'] = 'description'; return $map; } /** * Generate PidLidTimeZoneDescription string for a timezone * specified in a DateTime object */ protected static function timezone_description($datetime) { $timezone = $datetime->getTimezone(); $location = $timezone->getLocation(); $description = $location['comments']; $offset = $timezone->getOffset($datetime); // some location descriptions are useful, but some are not really // replace with timezone name in such cases if (!$description || strpos($description, 'location') !== false) { $description = $timezone->getName(); } if ($description == 'Z') { $description = 'UTC'; } // convert seconds into hours offset format $hours = round(abs($offset)/3600); $minutes = round((abs($offset) - $hours * 3600) / 60); $offset = sprintf('%s%02d:%02d', $offset < 0 ? '-' : '+', $hours, $minutes); return sprintf('(GMT%s) %s', $offset, $description); } /** * Generate PidLidTimeZoneDefinitionRecur blob for a timezone * specified in a DateTime object */ protected static function timezone_definition($datetime) { $timezone = $datetime->getTimezone(); $tzrule = kolab_api_filter_mapistore_structure_tzrule::from_datetime($datetime); $tzrule->Flags = kolab_api_filter_mapistore_structure_tzrule::FLAG_EFFECTIVE; // @FIXME $tzdef = new kolab_api_filter_mapistore_structure_timezonedefinition(array( 'TZRules' => array($tzrule), 'KeyName' => $timezone->getName(), )); return $tzdef->output(true); } /** * Generate PidLidTimeZoneStruct blob for a timezone * specified in a DateTime object */ protected static function timezone_structure($datetime) { $tzs = kolab_api_filter_mapistore_structure_timezonestruct::from_datetime($datetime, true); return $tzs->output(true); } /** * Parse PidLidTimeZoneStruct blob and convert to timezone name */ protected static function timezone_structure_to_tzname($data) { $api = kolab_api::get_instance(); $tzs = new kolab_api_filter_mapistore_structure_timezonestruct; $tzs->input($data, true); return $tzs->to_tzname($api->config->get('timezone')); } /** * Parse PidLidTimeZoneDefinitionRecur blob and convert to timezone name */ protected static function timezone_definition_to_tzname($data) { $api = kolab_api::get_instance(); $tzdef = new kolab_api_filter_mapistore_structure_timezonedefinition; $tzdef->input($data, true); // Note: we ignore KeyName as it most likely will not contain Olson TZ name foreach ($tzdef->TZRules as $tzrule) { if ($tzname = $tzrule->to_tzname($api->config->get('timezone'))) { return $tzname; } } } } diff --git a/lib/filter/mapistore/note.php b/lib/filter/mapistore/note.php index 00e6967..4c628fd 100644 --- a/lib/filter/mapistore/note.php +++ b/lib/filter/mapistore/note.php @@ -1,141 +1,147 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_note extends kolab_api_filter_mapistore_common { protected $model = 'note'; protected $map = array( // note specific props [MS-OXNOTE] 'PidLidNoteColor' => 'x-custom.MAPI:PidLidNoteColor', // PtypInteger32 'PidLidNoteHeight' => 'x-custom.MAPI:PidLidNoteHeight', // PtypInteger32 'PidLidNoteWidth' => 'x-custom.MAPI:PidLidNoteWidth', // PtypInteger32 'PidLidNoteX' => 'x-custom.MAPI:PidLidNoteX', // PtypInteger32 'PidLidNoteY' => 'x-custom.MAPI:PidLidNoteY', // PtypInteger32 // common props [MS-OXCMSG] - 'PidTagBody' => 'description', - 'PidTagHtml' => '', // @TODO: (?) + 'PidTagBody' => '', // 'description' + 'PidTagHtml' => '', 'PidTagMessageClass' => '', 'PidTagSubject' => 'summary', 'PidTagNormalizedSubject' => '', // @TODO: abbreviated note body 'PidTagIconIndex' => '', // @TODO: depends on PidLidNoteColor ); protected $color_map = array( '0000FF' => 0x00000000, // blue '008000' => 0x00000001, // green 'FFC0CB' => 0x00000002, // pink 'FFFF00' => 0x00000003, // yellow 'FFFFFF' => 0x00000004, // white ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { $result = array( 'PidTagMessageClass' => 'IPM.StickyNote', // notes do not have attachments in MAPI // 'PidTagHasAttachments' => 0, // mapistore REST API specific properties 'collection' => 'notes', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidLidNoteColor': case 'PidLidNoteHeight': case 'PidLidNoteWidth': case 'PidLidNoteX': case 'PidLidNoteY': $value = (int) $value; break; } $result[$mapi_idx] = $value; } + // body can be in plain text format only + $this->body_from_kolab($data, $result, 'description', 'plain'); + $this->parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; $result[$kolab_idx] = $value; } + // body + $this->body_to_kolab($data, $result, 'description'); + $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); return $map; } } diff --git a/lib/filter/mapistore/task.php b/lib/filter/mapistore/task.php index 775ef06..cb27330 100644 --- a/lib/filter/mapistore/task.php +++ b/lib/filter/mapistore/task.php @@ -1,294 +1,301 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_task extends kolab_api_filter_mapistore_common { protected $model = 'task'; protected $map = array( // task specific props [MS-OXOTASK] 'PidTagProcessed' => '', // PtypBoolean 'PidLidTaskMode' => '', // ignored 'PidLidTaskStatus' => '', // PtypInteger32 'PidLidPercentComplete' => 'percent-complete', // PtypFloating64 'PidLidTaskStartDate' => 'dtstart', // PtypTime 'PidLidTaskDueDate' => 'due', // PtypTime 'PidLidTaskResetReminder' => '', // @TODO // PtypBoolean 'PidLidTaskAccepted' => '', // @TODO // PtypBoolean 'PidLidTaskDeadOccurrence' => '', // @TODO // PtypBoolean 'PidLidTaskDateCompleted' => 'x-custom.MAPI:PidLidTaskDateCompleted', // PtypTime 'PidLidTaskLastUpdate' => '', // PtypTime 'PidLidTaskActualEffort' => 'x-custom.MAPI:PidLidTaskActualEffort', // PtypInteger32 'PidLidTaskEstimatedEffort' => 'x-custom.MAPI:PidLidTaskEstimatedEffort', // PtypInteger32 'PidLidTaskVersion' => '', // PtypInteger32 'PidLidTaskState' => '', // PtypInteger32 'PidLidTaskRecurrence' => '', // PtypBinary 'PidLidTaskAssigners' => '', // PtypBinary 'PidLidTaskStatusOnComplete' => '', // PtypBoolean 'PidLidTaskHistory' => '', // @TODO: ? // PtypInteger32 'PidLidTaskUpdates' => '', // PtypBoolean 'PidLidTaskComplete' => '', // PtypBoolean 'PidLidTaskFCreator' => '', // PtypBoolean 'PidLidTaskOwner' => '', // @TODO // PtypString 'PidLidTaskMultipleRecipients' => '', // PtypBoolean 'PidLidTaskAssigner' => '', // PtypString 'PidLidTaskLastUser' => '', // PtypString 'PidLidTaskOrdinal' => '', // PtypInteger32 'PidLidTaskLastDelegate' => '', // PtypString 'PidLidTaskFRecurring' => '', // PtypBoolean 'PidLidTaskOwnership' => '', // @TODO // PtypInteger32 'PidLidTaskAcceptanceState' => '', // PtypInteger32 'PidLidTaskFFixOffline' => '', // PtypBoolean 'PidLidTaskGlobalId' => '', // @TODO // PtypBinary 'PidLidTaskCustomFlags' => '', // ignored 'PidLidTaskRole' => '', // ignored 'PidLidTaskNoCompute' => '', // ignored 'PidLidTeamTask' => '', // ignored // common props [MS-OXCMSG] 'PidTagSubject' => 'summary', - 'PidTagBody' => 'description', - 'PidTagHtml' => '', // @TODO: (?) + 'PidTagBody' => '', + 'PidTagHtml' => '', 'PidTagNativeBody' => '', 'PidTagBodyHtml' => '', 'PidTagRtfCompressed' => '', 'PidTagInternetCodepage' => '', 'PidTagMessageClass' => '', 'PidLidCommonStart' => 'dtstart', 'PidLidCommonEnd' => 'due', 'PidTagIconIndex' => '', // @TODO 'PidTagCreationTime' => 'created', // PtypTime, UTC 'PidTagLastModificationTime' => 'dtstamp', // PtypTime, UTC ); /** * Values for PidLidTaskStatus property */ protected $status_map = array( 'none' => 0x00000000, // PidLidPercentComplete = 0 'in-progress' => 0x00000001, // PidLidPercentComplete > 0 and PidLidPercentComplete < 1 'complete' => 0x00000002, // PidLidPercentComplete = 1 'waiting' => 0x00000003, 'deferred' => 0x00000004, ); /** * Values for PidLidTaskHistory property */ protected $history_map = array( 'none' => 0x00000000, 'accepted' => 0x00000001, 'rejected' => 0x00000002, 'changed' => 0x00000003, 'due-changed' => 0x00000004, 'assigned' => 0x00000005, ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { $result = array( 'PidTagMessageClass' => 'IPM.Task', // mapistore REST API specific properties 'collection' => 'tasks', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidLidPercentComplete': $value /= 100; break; case 'PidLidTaskStartDate': case 'PidLidTaskDueDate': $value = $this->date_php2mapi($value, false, array('hour' => 0)); break; case 'PidLidCommonStart': case 'PidLidCommonEnd': // case 'PidLidTaskLastUpdate': case 'PidTagCreationTime': case 'PidTagLastModificationTime': $value = $this->date_php2mapi($value, true); break; case 'PidLidTaskActualEffort': case 'PidLidTaskEstimatedEffort': $value = (int) $value; break; } if ($value === null) { continue; } $result[$mapi_idx] = $value; } + // task description + $this->body_from_kolab($data, $result); + // set status $percent = $result['PidLidPercentComplete']; if ($precent == 1) { $result['PidLidTaskStatus'] = $this->status_map['complete']; // PidLidTaskDateCompleted (?) } else if ($precent > 0) { $result['PidLidTaskStatus'] = $this->status_map['in-progress']; } else { $result['PidLidTaskStatus'] = $this->status_map['none']; } // Organizer if (!empty($data['organizer'])) { $this->attendee_to_recipient($data['organizer'], $result, true); } // Attendees [MS-OXCICAL 2.1.3.1.1.20.2] foreach ((array) $data['attendee'] as $attendee) { $this->attendee_to_recipient($attendee, $result); } // Recurrence if ($rule = $this->recurrence_from_kolab($data, $result)) { $result['PidLidTaskRecurrence'] = $rule; $result['PidLidTaskFRecurring'] = true; } // Alarms (MAPI supports only one) $this->alarm_from_kolab($data, $result); $this->parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidLidPercentComplete': $value = intval($value * 100); break; case 'PidLidTaskStartDate': case 'PidLidTaskDueDate': if (intval($value) !== 0x5AE980E0) { $value = $this->date_mapi2php($value); $value = $value->format('Y-m-d'); } break; case 'PidLidCommonStart': case 'PidLidCommonEnd': // $value = $this->date_mapi2php($value, true); break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $value = $this->date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; } $result[$kolab_idx] = $value; } + // task description + $this->body_to_kolab($data, $result); + if ($data['PidLidTaskComplete']) { $result['status'] = 'COMPLETED'; } // Recurrence if (array_key_exists('PidLidTaskRecurrence', $data)) { $this->recurrence_to_kolab($data['PidLidTaskRecurrence'], $result, 'task'); } // Alarms (MAPI supports only one) $this->alarm_to_kolab($data, $result); if (array_key_exists('recipients', $data)) { $result['attendee'] = array(); $result['organizer'] = array(); foreach ((array) $data['recipients'] as $recipient) { $this->recipient_to_attendee($recipient, $result); } } $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); $map['PidLidTaskRecurrence'] = 'rrule'; + $map['PidTagBody'] = 'description'; return $map; } } diff --git a/tests/Unit/Filter/Mapistore/Event.php b/tests/Unit/Filter/Mapistore/Event.php index 1ee4ce7..e4922d6 100644 --- a/tests/Unit/Filter/Mapistore/Event.php +++ b/tests/Unit/Filter/Mapistore/Event.php @@ -1,422 +1,443 @@ output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', false, '100-100-100-100'), $result['id']); $this->assertSame('calendars', $result['collection']); $this->assertSame('IPM.Appointment', $result['PidTagMessageClass']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), $result['PidTagCreationTime']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:50:18Z'), $result['PidTagLastModificationTime']); $this->assertSame(30, $result['PidLidAppointmentDuration']); $this->assertSame(2, $result['PidLidAppointmentSequence']); $this->assertSame(3, $result['PidTagSensitivity']); $this->assertSame(true, $result['PidTagHasAttachments']); $this->assertSame(array('tag1'), $result['PidNameKeywords']); /* $this->assertSame('/kolab.org/Europe/Berlin', $result['dtstart']['parameters']['tzid']); $this->assertSame('2015-05-15T10:00:00', $result['dtstart']['date-time']); $this->assertSame('/kolab.org/Europe/Berlin', $result['dtend']['parameters']['tzid']); $this->assertSame('2015-05-15T10:30:00', $result['dtend']['date-time']); $this->assertSame('https://some.url', $result['url']); */ $this->assertSame('Summary', $result['PidTagSubject']); $this->assertSame('Description', $result['PidTagBody']); $this->assertSame(2, $result['PidTagImportance']); $this->assertSame('Location', $result['PidLidLocation']); $this->assertSame('German, Mark', $result['recipients'][0]['PidTagDisplayName']); $this->assertSame('mark.german@example.org', $result['recipients'][0]['PidTagEmailAddress']); $this->assertSame(1, $result['recipients'][0]['PidTagRecipientType']); $this->assertSame(3, $result['recipients'][0]['PidTagRecipientFlags']); $this->assertSame('Manager, Jane', $result['recipients'][1]['PidTagDisplayName']); $this->assertSame(1, $result['recipients'][1]['PidTagRecipientType']); $this->assertSame('jane.manager@example.org', $result['recipients'][1]['PidTagEmailAddress']); $this->assertSame(0, $result['recipients'][1]['PidTagRecipientTrackStatus']); $this->assertSame(1, $result['recipients'][1]['PidTagRecipientFlags']); $this->assertSame(15, $result['PidLidReminderDelta']); $this->assertSame(true, $result['PidLidReminderSet']); $this->assertTrue($result['PidLidAppointmentTimeZoneDefinitionStartDisplay'] == $result['PidLidAppointmentTimeZoneDefinitionStartDisplay']); // PidLidTimeZoneDefinition $tzd = new kolab_api_filter_mapistore_structure_timezonedefinition; $tzd->input($result['PidLidAppointmentTimeZoneDefinitionStartDisplay'], true); $this->assertSame('Europe/Berlin', $tzd->KeyName); $this->assertCount(1, $tzd->TZRules); $this->assertSame(2015, $tzd->TZRules[0]->Year); $this->assertSame(-60, $tzd->TZRules[0]->Bias); $data = kolab_api_tests::get_data('101-101-101-101', 'Calendar', 'event', 'json', $context); $result = $api->output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', false, '101-101-101-101'), $result['id']); $this->assertSame('calendars', $result['collection']); $this->assertSame('IPM.Appointment', $result['PidTagMessageClass']); $this->assertSame(0, $result['PidTagSensitivity']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T00:00:00Z'), $result['PidLidAppointmentStartWhole']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-16T00:00:00Z'), $result['PidLidAppointmentEndWhole']); $this->assertSame(24 * 60, $result['PidLidAppointmentDuration']); $this->assertSame(1, $result['PidLidAppointmentSubType']); $this->assertSame(null, $result['PidTagHasAttachments']); // EXDATE $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $arp->input($result['PidLidAppointmentRecur'], true); $this->assertSame(1, $arp->RecurrencePattern->Period); $this->assertSame(0x200B, $arp->RecurrencePattern->RecurFrequency); $this->assertSame(1, $arp->RecurrencePattern->PatternType); $this->assertSame(2, $arp->RecurrencePattern->DeletedInstanceCount); $this->assertCount(2, $arp->RecurrencePattern->DeletedInstanceDates); // RDATE $data = kolab_api_tests::get_data('102-102-102-102', 'Calendar', 'event', 'json', $context); $result = $api->output($data, $context); // recurrence $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $arp->input($result['PidLidAppointmentRecur'], true); $this->assertSame(2, $arp->RecurrencePattern->DeletedInstanceCount); $this->assertCount(2, $arp->RecurrencePattern->DeletedInstanceDates); $this->assertSame(2, $arp->RecurrencePattern->ModifiedInstanceCount); $this->assertCount(2, $arp->RecurrencePattern->ModifiedInstanceDates); $this->assertSame(2, $arp->ExceptionCount); $this->assertCount(2, $arp->ExceptionInfo); $this->assertCount(2, $arp->ExtendedException); // PidLidTimeZoneStruct $tz = new kolab_api_filter_mapistore_structure_timezonestruct; $tz->input($result['PidLidTimeZoneStruct'], true); $this->assertSame(-60, $tz->Bias); $this->assertSame(0, $tz->StandardYear); $this->assertSame(10, $tz->StandardDate->Month); $this->assertSame('(GMT+01:00) Europe/Berlin', $result['PidLidTimeZoneDescription']); } /** * Test recurrences output */ function test_output_recurrence() { $data = array( 'dtstart' => '2015-01-01T00:00:00Z', 'rrule' => array( 'recur' => array( 'freq' => 'MONTHLY', 'bymonthday' => 5, 'count' => 10, 'interval' => 2, ), ), 'exdate' => array( 'date' => array( '2015-01-01', '2016-01-01', ), ), 'rdate' => array( 'date' => array( '2015-02-01', ), ), ); $api = new kolab_api_filter_mapistore_event; $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $result = $api->output($data, $context); $arp->input($result['PidLidAppointmentRecur'], true); $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTH, $arp->RecurrencePattern->PatternType); $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_MONTHLY, $arp->RecurrencePattern->RecurFrequency); // @TODO: test mode recurrence exception details $this->assertSame(5, $arp->RecurrencePattern->PatternTypeSpecific); $this->assertSame(10, $arp->RecurrencePattern->OccurrenceCount); $this->assertSame(2, $arp->RecurrencePattern->Period); $this->assertSame(3, $arp->RecurrencePattern->DeletedInstanceCount); $this->assertCount(3, $arp->RecurrencePattern->DeletedInstanceDates); $this->assertSame(1, $arp->RecurrencePattern->ModifiedInstanceCount); $this->assertCount(1, $arp->RecurrencePattern->ModifiedInstanceDates); $this->assertSame(1, $arp->ExceptionCount); $this->assertCount(1, $arp->ExceptionInfo); $this->assertCount(1, $arp->ExtendedException); $this->assertSame(null, $result['PidLidAppointmentDuration']); } /** * Test alarms output */ function test_output_alarms() { kolab_api::$now = new DateTime('2015-01-20 00:00:00 UTC'); $data = array( 'dtstart' => '2015-01-01T00:00:00Z', 'rrule' => array( 'recur' => array( 'freq' => 'MONTHLY', 'bymonthday' => 5, 'count' => 10, 'interval' => 1, ), ), 'valarm' => array( array( 'properties' => array( 'action' => 'DISPLAY', 'trigger' => array( 'duration' => '-PT15M', ), ), ), ), 'duration' => 'PT10M', ); $api = new kolab_api_filter_mapistore_event; $result = $api->output($data, $context); $this->assertSame(15, $result['PidLidReminderDelta']); $this->assertSame(true, $result['PidLidReminderSet']); $this->assertSame('2015-02-20T00:00:00+00:00', kolab_api_filter_mapistore_common::date_mapi2php($result['PidLidReminderTime'])->format('c')); $this->assertSame('2015-02-19T23:45:00+00:00', kolab_api_filter_mapistore_common::date_mapi2php($result['PidLidReminderSignalTime'])->format('c')); $this->assertSame(10, $result['PidLidAppointmentDuration']); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_event; $tzs = new kolab_api_filter_mapistore_structure_timezonestruct(array( 'Bias' => -60, 'StandardBias' => 0, 'DaylightBias' => -60, 'StandardDate' => new kolab_api_filter_mapistore_structure_systemtime(array( 'Month' => 10, 'DayOfWeek' => 0, 'Day' => 5, 'Hour' => 3, )), 'DaylightDate' => new kolab_api_filter_mapistore_structure_systemtime(array( 'Month' => 3, 'Day' => 5, 'DayOfWeek' => 0, 'Hour' => 2, )), )); $data = array( 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:50:18Z'), 'PidLidAppointmentSequence' => 10, 'PidTagSensitivity' => 3, 'PidNameKeywords' => array('work'), 'PidTagSubject' => 'subject', 'PidTagBody' => 'body', 'PidTagImportance' => 2, 'PidLidLocation' => 'location', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T16:00:00Z'), 'PidLidReminderDelta' => 15, 'PidLidReminderSet' => true, 'PidLidTimeZoneStruct' => $tzs->output(true), 'recipients' => array( array( 'PidTagDisplayName' => 'German, Mark', 'PidTagEmailAddress' => 'mark.german@example.org', 'PidTagRecipientType' => 1, 'PidTagRecipientFlags' => 3, ), array( 'PidTagDisplayName' => 'Manager, Jane', 'PidTagEmailAddress' => 'manager@example.org', 'PidTagRecipientType' => 1, 'PidTagRecipientTrackStatus' => 2, ), ), ); $result = $api->input($data); $this->assertSame('subject', $result['summary']); $this->assertSame('body', $result['description']); $this->assertSame(10, $result['sequence']); $this->assertSame('confidential', $result['class']); $this->assertSame(array('work'), $result['categories']); $this->assertSame('location', $result['location']); $this->assertSame(1, $result['priority']); $this->assertSame('2015-05-14T13:03:33Z', $result['created']); $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); $this->assertSame('2015-05-14T15:03:33', $result['dtstart']['date-time']); $this->assertSame('2015-05-14T18:00:00', $result['dtend']['date-time']); $this->assertRegexp('/kolab.org/', $result['dtstart']['parameters']['tzid']); $this->assertRegexp('/kolab.org/', $result['dtend']['parameters']['tzid']); $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('-PT15M', $result['valarm'][0]['properties']['trigger']['duration']); $this->assertSame('Manager, Jane', $result['attendee'][0]['parameters']['cn']); $this->assertSame('TENTATIVE', $result['attendee'][0]['parameters']['partstat']); $this->assertSame('REQ-PARTICIPANT', $result['attendee'][0]['parameters']['role']); // $this->assertSame(true, $result['attendee'][0]['parameters']['rsvp']); $this->assertSame('mailto:manager%40example.org', $result['attendee'][0]['cal-address']); $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); $this->assertSame('mailto:mark.german%40example.org', $result['organizer']['cal-address']); self::$original = $result; $tzdef = base64_encode(pack("H*", '0201300002001500' . '500061006300690066006900630020005300740061006E0064006100720064002000540069006D006500' . '0100' . '02013E000000D6070000000000000000000000000000E001000000000000C4FFFFFF00000A0000000500020000000000000000000400000001000200000000000000' )); $data = array( 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T05:00:00Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T05:00:00Z'), 'PidLidAppointmentTimeZoneDefinitionStartDisplay' => $tzdef, 'PidLidReminderSet' => false, // @TODO: recurrence, exceptions, alarms ); $result = $api->input($data); $this->assertSame('2015-05-13T22:00:00', $result['dtstart']['date-time']); $this->assertSame('2015-05-14T05:00:00Z', $result['dtend']['date-time']); $this->assertSame(array(), $result['valarm']); } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_event; $data = array( // 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), // 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:50:18Z'), 'PidLidAppointmentSequence' => 20, 'PidTagSensitivity' => 2, 'PidNameKeywords' => array('work1'), 'PidTagSubject' => 'subject1', 'PidTagBody' => 'body1', 'PidTagImportance' => 1, 'PidLidLocation' => 'location1', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T13:03:33Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T16:00:00Z'), 'PidLidReminderDelta' => 25, 'PidLidReminderSet' => true, ); $result = $api->input($data, self::$original); $this->assertSame('subject1', $result['summary']); $this->assertSame('body1', $result['description']); $this->assertSame(20, $result['sequence']); $this->assertSame('private', $result['class']); $this->assertSame(array('work1'), $result['categories']); $this->assertSame('location1', $result['location']); $this->assertSame(5, $result['priority']); // $this->assertSame('2015-05-14T13:03:33Z', $result['created']); // $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); $this->assertSame('2015-05-15T13:03:33Z', $result['dtstart']['date-time']); $this->assertSame('2015-05-15T16:00:00Z', $result['dtend']['date-time']); $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('-PT25M', $result['valarm'][0]['properties']['trigger']['duration']); // @TODO: exceptions, attendees } /** * Test input recurrence */ function test_input_recurrence() { $api = new kolab_api_filter_mapistore_event; // build complete AppointmentRecurrencePattern structure $structure = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $exceptioninfo = new kolab_api_filter_mapistore_structure_exceptioninfo; $recurrencepattern = new kolab_api_filter_mapistore_structure_recurrencepattern; $extendedexception = new kolab_api_filter_mapistore_structure_extendedexception; $highlight = new kolab_api_filter_mapistore_structure_changehighlight; $highlight->ChangeHighlightValue = 4; $extendedexception->ChangeHighlight = $highlight; $extendedexception->StartDateTime = 0x0CBC9934; $extendedexception->EndDateTime = 0x0CBC9952; $extendedexception->OriginalStartDate = 0x0CBC98F8; $extendedexception->WideCharSubject = 'Simple Recurrence with exceptions'; $extendedexception->WideCharLocation = '34/4141'; $recurrencepattern->RecurFrequency = 0x200b; $recurrencepattern->PatternType = 1; $recurrencepattern->CalendarType = 0; $recurrencepattern->FirstDateTime = 0x000021C0; $recurrencepattern->Period = 1; $recurrencepattern->SlidingFlag = 0; $recurrencepattern->PatternTypeSpecific = 0x00000032; $recurrencepattern->EndType = 0x00002022; $recurrencepattern->OccurrenceCount = 12; $recurrencepattern->FirstDOW = 0; $recurrencepattern->DeletedInstanceDates = array(217742400, 218268000, 217787040); $recurrencepattern->ModifiedInstanceDates = array(217787040); $recurrencepattern->StartDate = 213655680; $recurrencepattern->EndDate = 0x0CBCAD20; $exceptioninfo->StartDateTime = 0x0CBC9934; $exceptioninfo->EndDateTime = 0x0CBC9952; $exceptioninfo->OriginalStartDate = 0x0CBC98F8; $exceptioninfo->Subject = 'Simple Recurrence with exceptions'; $exceptioninfo->Location = '34/4141'; $structure->StartTimeOffset = 600; $structure->EndTimeOffset = 630; $structure->ExceptionInfo = array($exceptioninfo); $structure->RecurrencePattern = $recurrencepattern; $structure->ExtendedException = array($extendedexception); $rule = $structure->output(true); $result = $api->input(array('PidLidAppointmentRecur' => $rule), $context); $this->assertSame('WEEKLY', $result['rrule']['recur']['freq']); $this->assertSame('SU', $result['rrule']['recur']['wkst']); $this->assertSame('SU,TU,MO,TH,FR', $result['rrule']['recur']['byday']); $this->assertSame(12, $result['rrule']['recur']['count']); $this->assertSame('2015-01-01', $result['exdate']['date'][0]); $this->assertSame('2016-01-01', $result['exdate']['date'][1]); $this->assertSame('2015-02-01', $result['rdate']['date'][0]); } + /** + * Test input body + */ + function test_input_body() + { + $api = new kolab_api_filter_mapistore_event; + $body = '0QAAAB0CAABMWkZ1Pzsq5D8ACQMwAQMB9wKnAgBjaBEKwHNldALRcHJx4DAgVGFoA3ECgwBQ6wNUDzcyD9MyBgAGwwKDpxIBA+MReDA0EhUgAoArApEI5jsJbzAVwzEyvjgJtBdCCjIXQRb0ORIAHxeEGOEYExjgFcMyNTX/CbQaYgoyGmEaHBaKCaUa9v8c6woUG3YdTRt/Hwwabxbt/xyPF7gePxg4JY0YVyRMKR+dJfh9CoEBMAOyMTYDMUksgSc1FGAnNhqAJ1Q3My3BNAqFfS7A'; + + $result = $api->input(array('PidTagRtfCompressed' => $body), $context); + + $this->assertSame('Test
', $result['description']); + + $result = $api->input(array('PidTagBody' => 'test'), $context); + + $this->assertSame('test', $result['description']); + + $result = $api->input(array('PidTagHtml' => 'test'), $context); + + $this->assertSame('test', $result['description']); + } + /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_event; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/Unit/Filter/Mapistore/Note.php b/tests/Unit/Filter/Mapistore/Note.php index cd40339..9f9cebe 100644 --- a/tests/Unit/Filter/Mapistore/Note.php +++ b/tests/Unit/Filter/Mapistore/Note.php @@ -1,132 +1,132 @@ output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Notes', false, '1-1-1-1'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('Notes', false), $result['parent_id']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-01-20T11:44:59Z'), $result['PidTagCreationTime']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-01-22T11:30:17Z'), $result['PidTagLastModificationTime']); // $this->assertSame('PUBLIC', $result['classification']); $this->assertSame('test', $result['PidTagSubject']); - $this->assertRegexp('//', $result['PidTagBody']); + $this->assertSame('test', $result['PidTagBody']); $this->assertSame(100, $result['PidLidNoteX']); $this->assertSame(200, $result['PidLidNoteY']); $this->assertSame(null, $result['PidTagHasAttachments']); $this->assertSame(array('tag1'), $result['PidNameKeywords']); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_note; $data = array( 'id' => kolab_api_tests::mapi_uid('Notes', false, '1-1-1-1'), 'parent_id' => kolab_api_tests::folder_uid('Notes', false), 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-20T11:44:59Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-22T11:30:17Z'), 'PidTagSubject' => 'subject', 'PidTagBody' => 'body', 'PidLidNoteColor' => 1, 'PidLidNoteHeight' => 100, 'PidLidNoteWidth' => 200, 'PidLidNoteX' => 300, 'PidLidNoteY' => 400, 'PidNameKeywords' => array('work1'), ); $result = $api->input($data); // $this->assertSame(kolab_api_tests::mapi_uid('Notes', false, '1-1-1-1'), $result['uid']); // $this->assertSame(kolab_api_tests::folder_uid('Notes', false), $result['parent']); $this->assertSame('2015-01-20T11:44:59Z', $result['creation-date']); $this->assertSame('2015-01-22T11:30:17Z', $result['last-modification-date']); $this->assertSame('subject', $result['summary']); $this->assertSame('body', $result['description']); $this->assertSame('MAPI:PidLidNoteColor', $result['x-custom'][0]['identifier']); $this->assertSame(1, $result['x-custom'][0]['value']); $this->assertSame('MAPI:PidLidNoteHeight', $result['x-custom'][1]['identifier']); $this->assertSame(100, $result['x-custom'][1]['value']); $this->assertSame('MAPI:PidLidNoteWidth', $result['x-custom'][2]['identifier']); $this->assertSame(200, $result['x-custom'][2]['value']); $this->assertSame('MAPI:PidLidNoteX', $result['x-custom'][3]['identifier']); $this->assertSame(300, $result['x-custom'][3]['value']); $this->assertSame('MAPI:PidLidNoteY', $result['x-custom'][4]['identifier']); $this->assertSame(400, $result['x-custom'][4]['value']); $this->assertSame(array('work1'), $result['categories']); self::$original = $result; } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_note; $data = array( 'id' => kolab_api_tests::mapi_uid('Notes', false, '1-1-1-1'), 'parent_id' => kolab_api_tests::folder_uid('Notes', false), 'PidTagSubject' => 'subject1', 'PidTagBody' => 'body1', 'PidLidNoteX' => 250, 'PidLidNoteColor' => null, ); $result = $api->input($data, self::$original); // $this->assertSame('2015-01-20T11:44:59Z', $result['creation-date']); // $this->assertSame('2015-01-22T11:30:17Z', $result['last-modification-date']); $this->assertSame('subject1', $result['summary']); $this->assertSame('body1', $result['description']); $this->assertSame('MAPI:PidLidNoteHeight', $result['x-custom'][0]['identifier']); $this->assertSame(100, $result['x-custom'][0]['value']); $this->assertSame('MAPI:PidLidNoteWidth', $result['x-custom'][1]['identifier']); $this->assertSame(200, $result['x-custom'][1]['value']); $this->assertSame('MAPI:PidLidNoteX', $result['x-custom'][2]['identifier']); $this->assertSame(250, $result['x-custom'][2]['value']); $this->assertSame('MAPI:PidLidNoteY', $result['x-custom'][3]['identifier']); $this->assertSame(400, $result['x-custom'][3]['value']); $this->assertCount(4, $result['x-custom']); // test unsetting values $api = new kolab_api_filter_mapistore_note; $data = array( 'PidTagSubject' => '', ); $result = $api->input($data, self::$original); $this->assertSame('', $result['summary']); } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_note; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/Unit/Filter/Mapistore/Task.php b/tests/Unit/Filter/Mapistore/Task.php index cb32640..7575484 100644 --- a/tests/Unit/Filter/Mapistore/Task.php +++ b/tests/Unit/Filter/Mapistore/Task.php @@ -1,267 +1,288 @@ output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Tasks', false, '10-10-10-10'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('Tasks', false), $result['parent_id']); $this->assertSame('IPM.Task', $result['PidTagMessageClass']); $this->assertSame('tasks', $result['collection']); $this->assertSame('task title', $result['PidTagSubject']); $this->assertSame("task description\nsecond line", $result['PidTagBody']); $this->assertSame(0.56, $result['PidLidPercentComplete']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-04-20T14:22:18Z', true), $result['PidTagLastModificationTime']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-04-20T14:22:18Z', true), $result['PidTagCreationTime']); $this->assertSame(8, $result['PidLidTaskActualEffort']); $this->assertSame(true, $result['PidTagHasAttachments']); $this->assertSame(array('tag1'), $result['PidNameKeywords']); $data = kolab_api_tests::get_data('20-20-20-20', 'Tasks', 'task', 'json', $context); $result = $api->output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Tasks', false, '20-20-20-20'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('Tasks', false), $result['parent_id']); $this->assertSame('IPM.Task', $result['PidTagMessageClass']); $this->assertSame('tasks', $result['collection']); $this->assertSame('task', $result['PidTagSubject']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-04-20', true), $result['PidLidTaskStartDate']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-04-27', true), $result['PidLidTaskDueDate']); $this->assertSame(null, $result['PidTagHasAttachments']); // organizer/attendees $this->assertSame('German, Mark', $result['recipients'][0]['PidTagDisplayName']); $this->assertSame('mark.german@example.org', $result['recipients'][0]['PidTagEmailAddress']); $this->assertSame(1, $result['recipients'][0]['PidTagRecipientType']); $this->assertSame('Manager, Jane', $result['recipients'][1]['PidTagDisplayName']); $this->assertSame(1, $result['recipients'][1]['PidTagRecipientType']); $this->assertSame('jane.manager@example.org', $result['recipients'][1]['PidTagEmailAddress']); // reminder $this->assertSame(15, $result['PidLidReminderDelta']); $this->assertSame(true, $result['PidLidReminderSet']); // recurrence $rp = new kolab_api_filter_mapistore_structure_recurrencepattern; $rp->input($result['PidLidTaskRecurrence'], true); $this->assertSame(true, $result['PidLidTaskFRecurring']); $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY, $rp->PatternType); $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_DAILY, $rp->RecurFrequency); } /** * Test output recurrence */ function test_output_recurrence() { // test task recurrence $data = array( 'dtstart' => '2015-01-01T00:00:00Z', 'rrule' => array( 'recur' => array( 'freq' => 'YEARLY', 'bymonth' => 5, 'bymonthday' => 1, 'count' => 10, ), ), ); $api = new kolab_api_filter_mapistore_task; $rp = new kolab_api_filter_mapistore_structure_recurrencepattern; $result = $api->output($data, $context); $rp->input($result['PidLidTaskRecurrence'], true); $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH, $rp->PatternType); $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_YEARLY, $rp->RecurFrequency); $this->assertSame(1, $rp->PatternTypeSpecific[1]); $this->assertSame(10, $rp->OccurrenceCount); $this->assertSame(12, $rp->Period); // @TODO: test other $rp properties } /** * Test alarms output */ function test_output_alarms() { kolab_api::$now = new DateTime('2015-01-20 00:00:00 UTC'); $data = array( 'dtstart' => '2015-01-01T00:00:00Z', 'rrule' => array( 'recur' => array( 'freq' => 'MONTHLY', 'bymonthday' => 5, 'count' => 10, 'interval' => 1, ), ), 'valarm' => array( array( 'properties' => array( 'action' => 'DISPLAY', 'trigger' => array( 'duration' => '-PT15M', ), ), ), ), ); $api = new kolab_api_filter_mapistore_task; $result = $api->output($data, $context); $this->assertSame(15, $result['PidLidReminderDelta']); $this->assertSame(true, $result['PidLidReminderSet']); $this->assertSame('2015-02-20T00:00:00+00:00', kolab_api_filter_mapistore_common::date_mapi2php($result['PidLidReminderTime'])->format('c')); $this->assertSame('2015-02-19T23:45:00+00:00', kolab_api_filter_mapistore_common::date_mapi2php($result['PidLidReminderSignalTime'])->format('c')); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_task; $data = array( 'id' => kolab_api_tests::mapi_uid('Tasks', false, '10-10-10-10'), 'parent_id' => kolab_api_tests::folder_uid('Tasks', false), 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-20T11:44:59Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-22T11:30:17Z'), 'PidTagMessageClass' => 'IPM.Task', 'PidTagSubject' => 'subject', 'PidLidPercentComplete' => 0.56, 'PidTagBody' => 'body', 'PidLidTaskStartDate' => kolab_api_filter_mapistore_common::date_php2mapi('2015-04-20', true), 'PidLidTaskDueDate' => kolab_api_filter_mapistore_common::date_php2mapi('2015-04-27', true), 'PidLidTaskActualEffort' => 16, 'PidLidTaskEstimatedEffort' => 20, 'PidLidReminderDelta' => 15, 'PidLidReminderSet' => true, 'PidNameKeywords' => array('work1'), 'recipients' => array( array( 'PidTagDisplayName' => 'German, Mark', 'PidTagEmailAddress' => 'mark.german@example.org', 'PidTagRecipientType' => 1, 'PidTagRecipientFlags' => 3, ), array( 'PidTagDisplayName' => 'Manager, Jane', 'PidTagEmailAddress' => 'manager@example.org', 'PidTagRecipientType' => 1, 'PidTagRecipientTrackStatus' => 2, ), ), ); $result = $api->input($data); self::$original = $result; $this->assertSame('subject', $result['summary']); $this->assertSame('body', $result['description']); $this->assertSame(56, $result['percent-complete']); $this->assertSame('2015-01-20T11:44:59Z', $result['created']); $this->assertSame('2015-01-22T11:30:17Z', $result['dtstamp']); $this->assertSame('2015-04-20', $result['dtstart']); $this->assertSame('2015-04-27', $result['due']); $this->assertSame('MAPI:PidLidTaskActualEffort', $result['x-custom'][0]['identifier']); $this->assertSame(16, $result['x-custom'][0]['value']); $this->assertSame('MAPI:PidLidTaskEstimatedEffort', $result['x-custom'][1]['identifier']); $this->assertSame(20, $result['x-custom'][1]['value']); $this->assertSame(array('work1'), $result['categories']); $this->assertSame('Manager, Jane', $result['attendee'][0]['parameters']['cn']); $this->assertSame('TENTATIVE', $result['attendee'][0]['parameters']['partstat']); $this->assertSame('REQ-PARTICIPANT', $result['attendee'][0]['parameters']['role']); // $this->assertSame(true, $result['attendee'][0]['parameters']['rsvp']); $this->assertSame('mailto:manager%40example.org', $result['attendee'][0]['cal-address']); $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); $this->assertSame('mailto:mark.german%40example.org', $result['organizer']['cal-address']); // alarm $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('-PT15M', $result['valarm'][0]['properties']['trigger']['duration']); $data = array( 'PidLidTaskComplete' => true, 'PidLidTaskDateCompleted' => kolab_api_filter_mapistore_common::date_php2mapi('2015-04-20', true), 'PidLidTaskActualEffort' => 100, 'PidLidTaskEstimatedEffort' => 100, // @TODO: recurrence ); $result = $api->input($data); $this->assertSame('COMPLETED', $result['status']); $this->assertSame('MAPI:PidLidTaskDateCompleted', $result['x-custom'][0]['identifier']); $this->assertSame(13073961600.0, $result['x-custom'][0]['value']); } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_task; $data = array( 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-20T12:44:59Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-22T12:30:17Z'), // 'PidTagMessageClass' => 'IPM.Task', 'PidTagSubject' => 'subject1', 'PidLidPercentComplete' => 0.66, 'PidTagBody' => 'body1', 'PidLidTaskStartDate' => kolab_api_filter_mapistore_common::date_php2mapi('2015-04-21', true), 'PidLidTaskDueDate' => kolab_api_filter_mapistore_common::date_php2mapi('2015-04-28', true), 'PidLidTaskActualEffort' => 21, 'PidLidTaskEstimatedEffort' => null, ); $result = $api->input($data, self::$original); self::$original = $result; $this->assertSame('subject1', $result['summary']); $this->assertSame('body1', $result['description']); $this->assertSame(66, $result['percent-complete']); $this->assertSame('2015-01-20T12:44:59Z', $result['created']); $this->assertSame('2015-01-22T12:30:17Z', $result['dtstamp']); $this->assertSame('2015-04-21', $result['dtstart']); $this->assertSame('2015-04-28', $result['due']); $this->assertSame('MAPI:PidLidTaskActualEffort', $result['x-custom'][0]['identifier']); $this->assertSame(21, $result['x-custom'][0]['value']); $this->assertCount(1, $result['x-custom']); } + /** + * Test input body + */ + function test_input_body() + { + $api = new kolab_api_filter_mapistore_task; + $body = '0QAAAB0CAABMWkZ1Pzsq5D8ACQMwAQMB9wKnAgBjaBEKwHNldALRcHJx4DAgVGFoA3ECgwBQ6wNUDzcyD9MyBgAGwwKDpxIBA+MReDA0EhUgAoArApEI5jsJbzAVwzEyvjgJtBdCCjIXQRb0ORIAHxeEGOEYExjgFcMyNTX/CbQaYgoyGmEaHBaKCaUa9v8c6woUG3YdTRt/Hwwabxbt/xyPF7gePxg4JY0YVyRMKR+dJfh9CoEBMAOyMTYDMUksgSc1FGAnNhqAJ1Q3My3BNAqFfS7A'; + + $result = $api->input(array('PidTagRtfCompressed' => $body), $context); + + $this->assertSame('Test
', $result['description']); + + $result = $api->input(array('PidTagBody' => 'test'), $context); + + $this->assertSame('test', $result['description']); + + $result = $api->input(array('PidTagHtml' => 'test'), $context); + + $this->assertSame('test', $result['description']); + } + /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_task; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } }