class.baserecurrence.php 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752
  1. <?php
  2. /*
  3. * Copyright 2005 - 2016 Zarafa and its licensors
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License, version 3,
  7. * as published by the Free Software Foundation.
  8. *
  9. * This program is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. * GNU Affero General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU Affero General Public License
  15. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. *
  17. */
  18. ?>
  19. <?php
  20. /**
  21. * BaseRecurrence
  22. * this class is superclass for recurrence for appointments and tasks. This class provides all
  23. * basic features of recurrence.
  24. */
  25. class BaseRecurrence {
  26. /**
  27. * @var object Mapi Message Store (may be null if readonly)
  28. */
  29. var $store;
  30. /**
  31. * @var object Mapi Message (may be null if readonly)
  32. */
  33. var $message;
  34. /**
  35. * @var array Message Properties
  36. */
  37. var $messageprops;
  38. /**
  39. * @var array list of property tags
  40. */
  41. var $proptags;
  42. /**
  43. * @var recurrence data of this calendar item
  44. */
  45. var $recur;
  46. /**
  47. * @var Timezone data of this calendar item
  48. */
  49. var $tz;
  50. /**
  51. * @param resource $store MAPI Message Store Object
  52. * @param resource $message the MAPI (appointment) message
  53. * @param array $properties the list of MAPI properties the message has.
  54. */
  55. function __construct($store, $message)
  56. {
  57. $this->store = $store;
  58. if(is_array($message)) {
  59. $this->messageprops = $message;
  60. } else {
  61. $this->message = $message;
  62. $this->messageprops = mapi_getprops($this->message, $this->proptags);
  63. }
  64. if(isset($this->messageprops[$this->proptags["recurring_data"]])) {
  65. // There is a possibility that recurr blob can be more than 255 bytes so get full blob through stream interface
  66. if (strlen($this->messageprops[$this->proptags["recurring_data"]]) >= 255)
  67. $this->getFullRecurrenceBlob();
  68. $this->recur = $this->parseRecurrence($this->messageprops[$this->proptags["recurring_data"]]);
  69. }
  70. if (isset($this->proptags["timezone_data"]) && isset($this->messageprops[$this->proptags["timezone_data"]]))
  71. $this->tz = $this->parseTimezone($this->messageprops[$this->proptags["timezone_data"]]);
  72. }
  73. function getRecurrence()
  74. {
  75. return $this->recur;
  76. }
  77. function getFullRecurrenceBlob()
  78. {
  79. $message = mapi_msgstore_openentry($this->store, $this->messageprops[PR_ENTRYID]);
  80. $recurrBlob = '';
  81. $stream = mapi_openproperty($message, $this->proptags["recurring_data"], IID_IStream, 0, 0);
  82. $stat = mapi_stream_stat($stream);
  83. for ($i = 0; $i < $stat['cb']; $i += 1024)
  84. $recurrBlob .= mapi_stream_read($stream, 1024);
  85. if (!empty($recurrBlob))
  86. $this->messageprops[$this->proptags["recurring_data"]] = $recurrBlob;
  87. }
  88. /**
  89. * Function for parsing the Recurrence value of a Calendar item.
  90. *
  91. * Retrieve it from Named Property 0x8216 as a PT_BINARY and pass the
  92. * data to this function
  93. *
  94. * Returns a structure containing the data:
  95. *
  96. * type - type of recurrence: day=10, week=11, month=12, year=13
  97. * subtype - type of day recurrence: 2=monthday (ie 21st day of month), 3=nday'th weekdays (ie. 2nd Tuesday and Wednesday)
  98. * start - Unix timestamp of first occurrence
  99. * end - Unix timestamp of last occurrence (up to and including), so when start == end -> occurrences = 1
  100. * numoccur - occurrences (may be very large when there is no end data)
  101. *
  102. * then, for each type:
  103. *
  104. * Daily:
  105. * everyn - every [everyn] days in minutes
  106. * regen - regenerating event (like tasks)
  107. *
  108. * Weekly:
  109. * everyn - every [everyn] weeks in weeks
  110. * regen - regenerating event (like tasks)
  111. * weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
  112. *
  113. * Monthly:
  114. * everyn - every [everyn] months
  115. * regen - regenerating event (like tasks)
  116. *
  117. * subtype 2:
  118. * monthday - on day [monthday] of the month
  119. *
  120. * subtype 3:
  121. * weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
  122. * nday - on [nday]'th [weekdays] of the month
  123. *
  124. * Yearly:
  125. * everyn - every [everyn] months (12, 24, 36, ...)
  126. * month - in month [month] (although the month is encoded in minutes since the startning of the year ........)
  127. * regen - regenerating event (like tasks)
  128. *
  129. * subtype 2:
  130. * monthday - on day [monthday] of the month
  131. *
  132. * subtype 3:
  133. * weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
  134. * nday - on [nday]'th [weekdays] of the month [month]
  135. * @param string $rdata Binary string
  136. * @return array recurrence data.
  137. */
  138. function parseRecurrence($rdata)
  139. {
  140. if (strlen($rdata) < 10)
  141. return;
  142. $ret["changed_occurences"] = array();
  143. $ret["deleted_occurences"] = array();
  144. $data = unpack("Vconst1/Crtype/Cconst2/Vrtype2", $rdata);
  145. $ret["type"] = $data["rtype"];
  146. $ret["subtype"] = $data["rtype2"];
  147. $rdata = substr($rdata, 10);
  148. switch ($data["rtype"])
  149. {
  150. case 0x0a:
  151. // Daily
  152. if (strlen($rdata) < 12)
  153. return $ret;
  154. $data = unpack("Vunknown/Veveryn/Vregen", $rdata);
  155. $ret["everyn"] = $data["everyn"];
  156. $ret["regen"] = $data["regen"];
  157. switch ($ret["subtype"])
  158. {
  159. case 0:
  160. $rdata = substr($rdata, 12);
  161. break;
  162. case 1:
  163. $rdata = substr($rdata, 16);
  164. break;
  165. }
  166. break;
  167. case 0x0b:
  168. // Weekly
  169. if (strlen($rdata) < 16)
  170. return $ret;
  171. $data = unpack("Vconst1/Veveryn/Vregen", $rdata);
  172. $rdata = substr($rdata, 12);
  173. $ret["everyn"] = $data["everyn"];
  174. $ret["regen"] = $data["regen"];
  175. $ret["weekdays"] = 0;
  176. if ($data["regen"] == 0) {
  177. $data = unpack("Vweekdays", $rdata);
  178. $rdata = substr($rdata, 4);
  179. $ret["weekdays"] = $data["weekdays"];
  180. }
  181. break;
  182. case 0x0c:
  183. // Monthly
  184. if (strlen($rdata) < 16)
  185. return $ret;
  186. $data = unpack("Vconst1/Veveryn/Vregen/Vmonthday", $rdata);
  187. $ret["everyn"] = $data["everyn"];
  188. $ret["regen"] = $data["regen"];
  189. if ($ret["subtype"] == 3)
  190. $ret["weekdays"] = $data["monthday"];
  191. else
  192. $ret["monthday"] = $data["monthday"];
  193. $rdata = substr($rdata, 16);
  194. if ($ret["subtype"] == 3) {
  195. $data = unpack("Vnday", $rdata);
  196. $ret["nday"] = $data["nday"];
  197. $rdata = substr($rdata, 4);
  198. }
  199. break;
  200. case 0x0d:
  201. // Yearly
  202. if (strlen($rdata) < 16)
  203. return $ret;
  204. $data = unpack("Vmonth/Veveryn/Vregen/Vmonthday", $rdata);
  205. $ret["month"] = $data["month"];
  206. $ret["everyn"] = $data["everyn"];
  207. $ret["regen"] = $data["regen"];
  208. if ($ret["subtype"] == 3)
  209. $ret["weekdays"] = $data["monthday"];
  210. else
  211. $ret["monthday"] = $data["monthday"];
  212. $rdata = substr($rdata, 16);
  213. if ($ret["subtype"] == 3) {
  214. $data = unpack("Vnday", $rdata);
  215. $ret["nday"] = $data["nday"];
  216. $rdata = substr($rdata, 4);
  217. }
  218. break;
  219. }
  220. if (strlen($rdata) < 16)
  221. return $ret;
  222. $data = unpack("Cterm/C3const1/Vnumoccur/Vconst2/Vnumexcept", $rdata);
  223. $rdata = substr($rdata, 16);
  224. $ret["term"] = $data["term"];
  225. $ret["numoccur"] = $data["numoccur"];
  226. $ret["numexcept"] = $data["numexcept"];
  227. // exc_base_dates are *all* the base dates that have been either deleted or modified
  228. $exc_base_dates = array();
  229. for($i = 0; $i < $ret["numexcept"]; $i++)
  230. {
  231. if (strlen($rdata) < 4)
  232. // We shouldn't arrive here, because that implies
  233. // numexcept does not match the amount of data
  234. // which is available for the exceptions.
  235. return $ret;
  236. $data = unpack("Vbasedate", $rdata);
  237. $rdata = substr($rdata, 4);
  238. $exc_base_dates[] = $this->recurDataToUnixData($data["basedate"]);
  239. }
  240. if (strlen($rdata) < 4)
  241. return $ret;
  242. $data = unpack("Vnumexceptmod", $rdata);
  243. $rdata = substr($rdata, 4);
  244. $ret["numexceptmod"] = $data["numexceptmod"];
  245. // exc_changed are the base dates of *modified* occurrences. exactly what is modified
  246. // is in the attachments *and* in the data further down this function.
  247. $exc_changed = array();
  248. for($i = 0; $i < $ret["numexceptmod"]; $i++)
  249. {
  250. if (strlen($rdata) < 4)
  251. // We shouldn't arrive here, because that implies
  252. // numexceptmod does not match the amount of data
  253. // which is available for the exceptions.
  254. return $ret;
  255. $data = unpack("Vstartdate", $rdata);
  256. $rdata = substr($rdata, 4);
  257. $exc_changed[] = $this->recurDataToUnixData($data["startdate"]);
  258. }
  259. if (strlen($rdata) < 8)
  260. return $ret;
  261. $data = unpack("Vstart/Vend", $rdata);
  262. $rdata = substr($rdata, 8);
  263. $ret["start"] = $this->recurDataToUnixData($data["start"]);
  264. $ret["end"] = $this->recurDataToUnixData($data["end"]);
  265. // this is where task recurrence stop
  266. if (strlen($rdata) < 16)
  267. return $ret;
  268. $data = unpack("Vreaderversion/Vwriterversion/Vstartmin/Vendmin", $rdata);
  269. $rdata = substr($rdata, 16);
  270. $ret["startocc"] = $data["startmin"];
  271. $ret["endocc"] = $data["endmin"];
  272. $readerversion = $data["readerversion"];
  273. $writerversion = $data["writerversion"];
  274. $data = unpack("vnumber", $rdata);
  275. $rdata = substr($rdata, 2);
  276. $nexceptions = $data["number"];
  277. $exc_changed_details = array();
  278. // Parse n modified exceptions
  279. for($i=0;$i<$nexceptions;$i++)
  280. {
  281. $item = array();
  282. // Get exception startdate, enddate and basedate (the date at which the occurrence would have started)
  283. $data = unpack("Vstartdate/Venddate/Vbasedate", $rdata);
  284. $rdata = substr($rdata, 12);
  285. // Convert recurtimestamp to Unix timestamp
  286. $startdate = $this->recurDataToUnixData($data["startdate"]);
  287. $enddate = $this->recurDataToUnixData($data["enddate"]);
  288. $basedate = $this->recurDataToUnixData($data["basedate"]);
  289. // Set the right properties
  290. $item["basedate"] = $this->dayStartOf($basedate);
  291. $item["start"] = $startdate;
  292. $item["end"] = $enddate;
  293. $data = unpack("vbitmask", $rdata);
  294. $rdata = substr($rdata, 2);
  295. $item["bitmask"] = $data["bitmask"]; // save bitmask for extended exceptions
  296. // Bitmask to verify what properties are changed
  297. $bitmask = $data["bitmask"];
  298. // ARO_SUBJECT: 0x0001
  299. // Look for field: SubjectLength (2b), SubjectLength2 (2b) and Subject
  300. if(($bitmask &(1 << 0))) {
  301. $data = unpack("vnull_length/vlength", $rdata);
  302. $rdata = substr($rdata, 4);
  303. $length = $data["length"];
  304. $item["subject"] = ""; // Normalized subject
  305. for($j = 0; $j < $length && strlen($rdata); $j++)
  306. {
  307. $data = unpack("Cchar", $rdata);
  308. $rdata = substr($rdata, 1);
  309. $item["subject"] .= chr($data["char"]);
  310. }
  311. }
  312. // ARO_MEETINGTYPE: 0x0002
  313. if (($bitmask & (1 << 1)))
  314. $rdata = substr($rdata, 4);
  315. // Attendees modified: no data here (only in attachment)
  316. // ARO_REMINDERDELTA: 0x0004
  317. // Look for field: ReminderDelta (4b)
  318. if(($bitmask &(1 << 2))) {
  319. $data = unpack("Vremind_before", $rdata);
  320. $rdata = substr($rdata, 4);
  321. $item["remind_before"] = $data["remind_before"];
  322. }
  323. // ARO_REMINDER: 0x0008
  324. // Look field: ReminderSet (4b)
  325. if(($bitmask &(1 << 3))) {
  326. $data = unpack("Vreminder_set", $rdata);
  327. $rdata = substr($rdata, 4);
  328. $item["reminder_set"] = $data["reminder_set"];
  329. }
  330. // ARO_LOCATION: 0x0010
  331. // Look for fields: LocationLength (2b), LocationLength2 (2b) and Location
  332. // Similar to ARO_SUBJECT above.
  333. if(($bitmask &(1 << 4))) {
  334. $data = unpack("vnull_length/vlength", $rdata);
  335. $rdata = substr($rdata, 4);
  336. $item["location"] = "";
  337. $length = $data["length"];
  338. $data = substr($rdata, 0, $length);
  339. $rdata = substr($rdata, $length);
  340. $item["location"] .= $data;
  341. }
  342. // ARO_BUSYSTATUS: 0x0020
  343. // Look for field: BusyStatus (4b)
  344. if(($bitmask &(1 << 5))) {
  345. $data = unpack("Vbusystatus", $rdata);
  346. $rdata = substr($rdata, 4);
  347. $item["busystatus"] = $data["busystatus"];
  348. }
  349. // ARO_ATTACHMENT: 0x0040
  350. if (($bitmask &(1 << 6)))
  351. // no data: RESERVED
  352. $rdata = substr($rdata, 4);
  353. // ARO_SUBTYPE: 0x0080
  354. // Look for field: SubType (4b). Determines whether it is an allday event.
  355. if(($bitmask &(1 << 7))) {
  356. $data = unpack("Vallday", $rdata);
  357. $rdata = substr($rdata, 4);
  358. $item["alldayevent"] = $data["allday"];
  359. }
  360. // ARO_APPTCOLOR: 0x0100
  361. // Look for field: AppointmentColor (4b)
  362. if(($bitmask &(1 << 8))) {
  363. $data = unpack("Vlabel", $rdata);
  364. $rdata = substr($rdata, 4);
  365. $item["label"] = $data["label"];
  366. }
  367. // ARO_EXCEPTIONAL_BODY: 0x0200
  368. if (($bitmask & (1 << 9)))
  369. /*nothing*/; // Notes or Attachments modified: no data here (only in attachment)
  370. array_push($exc_changed_details, $item);
  371. }
  372. /**
  373. * We now have $exc_changed, $exc_base_dates and $exc_changed_details
  374. * We will ignore $exc_changed, as this information is available in $exc_changed_details
  375. * also. If an item is in $exc_base_dates and NOT in $exc_changed_details, then the item
  376. * has been deleted.
  377. */
  378. // Find deleted occurrences
  379. $deleted_occurences = array();
  380. foreach($exc_base_dates as $base_date) {
  381. $found = false;
  382. foreach($exc_changed_details as $details) {
  383. if($details["basedate"] == $base_date) {
  384. $found = true;
  385. break;
  386. }
  387. }
  388. if (!$found)
  389. // item was not in exc_changed_details, so it must be deleted
  390. $deleted_occurences[] = $base_date;
  391. }
  392. $ret["deleted_occurences"] = $deleted_occurences;
  393. $ret["changed_occurences"] = $exc_changed_details;
  394. // enough data for normal exception (no extended data)
  395. if (strlen($rdata) < 16)
  396. return $ret;
  397. $data = unpack("Vreservedsize", $rdata);
  398. $rdata = substr($rdata, 4 + $data["reservedsize"]);
  399. for($i=0;$i<$nexceptions;$i++)
  400. {
  401. // subject and location in UCS-2 to UTF-8
  402. if ($writerversion >= 0x3009) {
  403. $data = unpack("Vsize/Vvalue", $rdata); // size includes sizeof(value)==4
  404. $rdata = substr($rdata, 4 + $data["size"]);
  405. }
  406. $data = unpack("Vreservedsize", $rdata);
  407. $rdata = substr($rdata, 4 + $data["reservedsize"]);
  408. // ARO_SUBJECT(0x01) | ARO_LOCATION(0x10)
  409. if ($exc_changed_details[$i]["bitmask"] & 0x11) {
  410. $data = unpack("Vstart/Vend/Vorig", $rdata);
  411. $rdata = substr($rdata, 4 * 3);
  412. $exc_changed_details[$i]["ex_start_datetime"] = $data["start"];
  413. $exc_changed_details[$i]["ex_end_datetime"] = $data["end"];
  414. $exc_changed_details[$i]["ex_orig_date"] = $data["orig"];
  415. }
  416. // ARO_SUBJECT
  417. if ($exc_changed_details[$i]["bitmask"] & 0x01) {
  418. // decode UCS-2 string to UTF-8
  419. $data = unpack("vlength", $rdata);
  420. $rdata = substr($rdata, 2);
  421. $length = $data["length"];
  422. $data = substr($rdata, 0, $length * 2);
  423. $rdata = substr($rdata, $length * 2);
  424. $subject = iconv("UCS-2LE", "UTF-8", $data);
  425. // replace subject with unicode subject
  426. $exc_changed_details[$i]["subject"] = $subject;
  427. }
  428. // ARO_LOCATION
  429. if ($exc_changed_details[$i]["bitmask"] & 0x10) {
  430. // decode UCS-2 string to UTF-8
  431. $data = unpack("vlength", $rdata);
  432. $rdata = substr($rdata, 2);
  433. $length = $data["length"];
  434. $data = substr($rdata, 0, $length * 2);
  435. $rdata = substr($rdata, $length * 2);
  436. $location = iconv("UCS-2LE", "UTF-8", $data);
  437. // replace subject with unicode subject
  438. $exc_changed_details[$i]["location"] = $location;
  439. }
  440. // ARO_SUBJECT(0x01) | ARO_LOCATION(0x10)
  441. if ($exc_changed_details[$i]["bitmask"] & 0x11) {
  442. $data = unpack("Vreservedsize", $rdata);
  443. $rdata = substr($rdata, 4 + $data["reservedsize"]);
  444. }
  445. }
  446. // update with extended data
  447. $ret["changed_occurences"] = $exc_changed_details;
  448. return $ret;
  449. }
  450. /**
  451. * Saves the recurrence data to the recurrence property
  452. * @param array $properties the recurrence data.
  453. * @return string binary string
  454. */
  455. function saveRecurrence()
  456. {
  457. // Only save if a message was passed
  458. if(!isset($this->message))
  459. return;
  460. // Abort if no recurrence was set
  461. if (!isset($this->recur["type"]) && !isset($this->recur["subtype"]))
  462. return;
  463. if (!isset($this->recur["start"]) && !isset($this->recur["end"]))
  464. return;
  465. if (!isset($this->recur["startocc"]) && !isset($this->recur["endocc"]))
  466. return;
  467. $rdata = pack("CCCCCCV", 0x04, 0x30, 0x04, 0x30, (int) $this->recur["type"], 0x20, (int) $this->recur["subtype"]);
  468. $weekstart = 1; //monday
  469. $forwardcount = 0;
  470. $restocc = 0;
  471. $dayofweek = (int) gmdate("w", (int) $this->recur["start"]); //0 (for Sunday) through 6 (for Saturday)
  472. $term = (int) $this->recur["type"];
  473. switch($term)
  474. {
  475. case 0x0A:
  476. // Daily
  477. if (!isset($this->recur["everyn"]))
  478. return;
  479. if ($this->recur["subtype"] == 1) {
  480. // Daily every workday
  481. $rdata .= pack("VVVV", (6 * 24 * 60), 1, 0, 0x3E);
  482. } else {
  483. // Daily every N days (everyN in minutes)
  484. $everyn = ((int) $this->recur["everyn"]) / 1440;
  485. // Calc first occ
  486. $firstocc = $this->unixDataToRecurData($this->recur["start"]) % ((int) $this->recur["everyn"]);
  487. $rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], $this->recur["regen"] ? 1 : 0);
  488. }
  489. break;
  490. case 0x0B:
  491. // Weekly
  492. if (!isset($this->recur["everyn"]))
  493. return;
  494. if (!$this->recur["regen"] && !isset($this->recur["weekdays"]))
  495. return;
  496. // No need to calculate startdate if sliding flag was set.
  497. if (!$this->recur['regen']) {
  498. // Calculate start date of recurrence
  499. // Find the first day that matches one of the weekdays selected
  500. $daycount = 0;
  501. $dayskip = -1;
  502. for ($j = 0; $j < 7; $j++) {
  503. if (((int) $this->recur["weekdays"]) & (1 << (($dayofweek + $j) % 7))) {
  504. if ($dayskip == -1)
  505. $dayskip = $j;
  506. $daycount++;
  507. }
  508. }
  509. // $dayskip is the number of days to skip from the startdate until the first occurrence
  510. // $daycount is the number of days per week that an occurrence occurs
  511. $weekskip = 0;
  512. if (($dayofweek < $weekstart && $dayskip > 0) || ($dayofweek + $dayskip) > 6)
  513. $weekskip = 1;
  514. // Check if the recurrence ends after a number of occurences, in that case we must calculate the
  515. // remaining occurences based on the start of the recurrence.
  516. if (((int) $this->recur["term"]) == 0x22) {
  517. // $weekskip is the amount of weeks to skip from the startdate before the first occurence
  518. // $forwardcount is the maximum number of week occurrences we can go ahead after the first occurrence that
  519. // is still inside the recurrence. We subtract one to make sure that the last week is never forwarded over
  520. // (eg when numoccur = 2, and daycount = 1)
  521. $forwardcount = floor((int) ($this->recur["numoccur"] - 1) / $daycount);
  522. // $restocc is the number of occurrences left after $forwardcount whole weeks of occurrences, minus one
  523. // for the occurrence on the first day
  524. $restocc = ((int) $this->recur["numoccur"]) - ($forwardcount * $daycount) - 1;
  525. // $forwardcount is now the number of weeks we can go forward and still be inside the recurrence
  526. $forwardcount *= (int) $this->recur["everyn"];
  527. }
  528. // The real start is start + dayskip + weekskip-1 (since dayskip will already bring us into the next week)
  529. $this->recur["start"] = ((int) $this->recur["start"]) + ($dayskip * 24 * 60 * 60)+ ($weekskip * (((int) $this->recur["everyn"]) - 1) * 7 * 24 * 60 * 60);
  530. }
  531. // Calc first occ
  532. $firstocc = ($this->unixDataToRecurData($this->recur["start"])) % (((int) $this->recur["everyn"]) * 7 * 24 * 60);
  533. $firstocc -= (((int) gmdate("w", (int) $this->recur["start"])) - 1) * 24 * 60;
  534. if ($this->recur["regen"])
  535. $rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], 1);
  536. else
  537. $rdata .= pack("VVVV", $firstocc, (int) $this->recur["everyn"], 0, (int) $this->recur["weekdays"]);
  538. break;
  539. case 0x0C:
  540. // Monthly
  541. case 0x0D:
  542. // Yearly
  543. if (!isset($this->recur["everyn"]))
  544. return;
  545. if ($term == 0x0D /*yearly*/ && !isset($this->recur["month"]))
  546. return;
  547. if ($term == 0x0C /*monthly*/)
  548. $everyn = (int) $this->recur["everyn"];
  549. else
  550. $everyn = $this->recur["regen"] ? ((int) $this->recur["everyn"]) * 12 : 12;
  551. // Get montday/month/year of original start
  552. $curmonthday = gmdate("j", (int) $this->recur["start"]);
  553. $curyear = gmdate("Y", (int) $this->recur["start"]);
  554. $curmonth = gmdate("n", (int) $this->recur["start"]);
  555. // Check if the recurrence ends after a number of occurences, in that case we must calculate the
  556. // remaining occurences based on the start of the recurrence.
  557. if (((int) $this->recur["term"]) == 0x22)
  558. // $forwardcount is the number of occurrences we can skip and still be inside the recurrence range (minus
  559. // one to make sure there are always at least one occurrence left)
  560. $forwardcount = ((((int) $this->recur["numoccur"]) - 1) * $everyn);
  561. // Get month for yearly on D'th day of month M
  562. if ($term == 0x0D /*yearly*/)
  563. $selmonth = floor(((int) $this->recur["month"]) / (24 * 60 *29)) + 1; // 1=jan, 2=feb, eg
  564. switch ((int) $this->recur["subtype"])
  565. {
  566. // on D day of every M month
  567. case 2:
  568. if (!isset($this->recur["monthday"]))
  569. return;
  570. // Recalc startdate
  571. // Set on the right begin day
  572. // Go the beginning of the month
  573. $this->recur["start"] -= ($curmonthday-1) * 24 * 60 * 60;
  574. // Go the the correct month day
  575. $this->recur["start"] += (((int) $this->recur["monthday"]) - 1) * 24 * 60 * 60;
  576. // If the previous calculation gave us a start date *before* the original start date, then we need to skip to the next occurrence
  577. if (($term == 0x0C /*monthly*/ && ((int) $this->recur["monthday"]) < $curmonthday) ||
  578. ($term == 0x0D /*yearly*/ && ($selmonth < $curmonth || ($selmonth == $curmonth && ((int) $this->recur["monthday"]) < $curmonthday))))
  579. {
  580. if ($term == 0x0D /*yearly*/)
  581. $count = ($everyn - ($curmonth - $selmonth)); // Yearly, go to next occurrence in 'everyn' months minus difference in first occurence and original date
  582. else
  583. $count = $everyn; // Monthly, go to next occurrence in 'everyn' months
  584. // Forward by $count months. This is done by getting the number of days in that month and forwarding that many days
  585. for ($i = 0; $i < $count; $i++) {
  586. $this->recur["start"] += $this->getMonthInSeconds($curyear, $curmonth);
  587. if ($curmonth == 12) {
  588. $curyear++;
  589. $curmonth = 0;
  590. }
  591. $curmonth++;
  592. }
  593. }
  594. // "start" is now pointing to the first occurrence, except that it will overshoot if the
  595. // month in which it occurs has less days than specified as the day of the month. So 31st
  596. // of each month will overshoot in february (29 days). We compensate for that by checking
  597. // if the day of the month we got is wrong, and then back up to the last day of the previous
  598. // month.
  599. if (((int) $this->recur["monthday"]) >= 28 && ((int) $this->recur["monthday"]) <= 31 &&
  600. gmdate("j", ((int) $this->recur["start"])) < ((int) $this->recur["monthday"]))
  601. $this->recur["start"] -= gmdate("j", ((int) $this->recur["start"])) * 24 * 60 * 60;
  602. // "start" is now the first occurrence
  603. if ($term == 0x0C /*monthly*/) {
  604. // Calc first occ
  605. $monthIndex = ((((12 % $everyn) * ((((int) gmdate("Y", $this->recur["start"])) - 1601)%$everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1)) % $everyn;
  606. $firstocc = 0;
  607. for ($i = 0; $i < $monthIndex; $i++)
  608. $firstocc+= $this->getMonthInSeconds(1601 + floor($i / 12), ($i % 12) + 1) / 60;
  609. $rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]);
  610. } else{
  611. // Calc first occ
  612. $firstocc = 0;
  613. $monthIndex = (int) gmdate("n", $this->recur["start"]);
  614. for ($i = 1; $i < $monthIndex; $i++)
  615. $firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), $i) / 60;
  616. $rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]);
  617. }
  618. break;
  619. case 3:
  620. // monthly: on Nth weekday of every M month
  621. // yearly: on Nth weekday of M month
  622. if (!isset($this->recur["weekdays"]) && !isset($this->recur["nday"]))
  623. return;
  624. $weekdays = (int) $this->recur["weekdays"];
  625. $nday = (int) $this->recur["nday"];
  626. // Calc startdate
  627. $monthbegindow = (int) $this->recur["start"];
  628. if ($nday == 5)
  629. // Set date on the last day of the last month
  630. $monthbegindow += (gmdate("t", $monthbegindow ) - gmdate("j", $monthbegindow )) * 24 * 60 * 60;
  631. else
  632. // Set on the first day of the month
  633. $monthbegindow -= ((gmdate("j", $monthbegindow )-1) * 24 * 60 * 60);
  634. if ($term == 0x0D /*yearly*/) {
  635. // Set on right month
  636. if ($selmonth < $curmonth)
  637. $tmp = 12 - $curmonth + $selmonth;
  638. else
  639. $tmp = ($selmonth - $curmonth);
  640. for ($i = 0; $i < $tmp; $i++) {
  641. $monthbegindow += $this->getMonthInSeconds($curyear, $curmonth);
  642. if ($curmonth == 12) {
  643. $curyear++;
  644. $curmonth = 0;
  645. }
  646. $curmonth++;
  647. }
  648. } else {
  649. // Check or you exist in the right month
  650. for ($i = 0; $i < 7; $i++) {
  651. if ($nday == 5 && (1 << ((gmdate("w", $monthbegindow) - $i) % 7)) & $weekdays) {
  652. $day = gmdate("j", $monthbegindow) - $i;
  653. break;
  654. } else if ($nday != 5 && (1 << ((gmdate("w", $monthbegindow) + $i) % 7)) & $weekdays) {
  655. $day = (($nday - 1) * 7) + ($i + 1);
  656. break;
  657. }
  658. }
  659. // Goto the next X month
  660. if (isset($day) && ($day < gmdate("j", (int) $this->recur["start"]))) {
  661. if ($nday == 5) {
  662. $monthbegindow += 24 * 60 * 60;
  663. if ($curmonth == 12) {
  664. $curyear++;
  665. $curmonth = 0;
  666. }
  667. $curmonth++;
  668. }
  669. for ($i = 0; $i < $everyn; $i++) {
  670. $monthbegindow += $this->getMonthInSeconds($curyear, $curmonth);
  671. if ($curmonth == 12) {
  672. $curyear++;
  673. $curmonth = 0;
  674. }
  675. $curmonth++;
  676. }
  677. if ($nday == 5)
  678. $monthbegindow -= 24 * 60 * 60;
  679. }
  680. }
  681. //FIXME: weekstart?
  682. $day = 0;
  683. // Set start on the right day
  684. for ($i = 0; $i < 7; $i++) {
  685. if ($nday == 5 && (1 << ((gmdate("w", $monthbegindow) - $i) % 7)) & $weekdays) {
  686. $day = $i;
  687. break;
  688. } else if ($nday != 5 && (1 << ((gmdate("w", $monthbegindow) + $i) % 7)) & $weekdays) {
  689. $day = ($nday - 1) * 7 + ($i + 1);
  690. break;
  691. }
  692. }
  693. if ($nday == 5)
  694. $monthbegindow -= $day * 24 * 60 * 60;
  695. else
  696. $monthbegindow += ($day - 1) * 24 * 60 * 60;
  697. $firstocc = 0;
  698. if ($term == 0x0C /*monthly*/) {
  699. // Calc first occ
  700. $monthIndex = ((((12 % $everyn) * (((int) gmdate("Y", $this->recur["start"]) - 1601) % $everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1)) % $everyn;
  701. for ($i = 0; $i < $monthIndex; $i++)
  702. $firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), ($i % 12) + 1) / 60;
  703. $rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday);
  704. break;
  705. }
  706. // Calc first occ
  707. $monthIndex = (int) gmdate("n", $this->recur["start"]);
  708. for ($i = 1; $i < $monthIndex; $i++)
  709. $firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), $i) / 60;
  710. $rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday);
  711. break;
  712. }
  713. break;
  714. }
  715. if (!isset($this->recur["term"]))
  716. return;
  717. // Terminate
  718. $term = (int) $this->recur["term"];
  719. $rdata .= pack("CCCC", $term, 0x20, 0x00, 0x00);
  720. switch($term)
  721. {
  722. // After the given enddate
  723. case 0x21:
  724. $rdata .= pack("V", 10);
  725. break;
  726. // After a number of times
  727. case 0x22:
  728. if (!isset($this->recur["numoccur"]))
  729. return;
  730. $rdata .= pack("V", (int) $this->recur["numoccur"]);
  731. break;
  732. // Never ends
  733. case 0x23:
  734. $rdata .= pack("V", 0);
  735. break;
  736. }
  737. // Strange little thing for the recurrence type "every workday"
  738. if (((int) $this->recur["type"]) == 0x0B && ((int) $this->recur["subtype"]) == 1)
  739. $rdata .= pack("V", 1);
  740. else // Other recurrences
  741. $rdata .= pack("V", 0);
  742. // Exception data
  743. // Get all exceptions
  744. $deleted_items = $this->recur["deleted_occurences"];
  745. $changed_items = $this->recur["changed_occurences"];
  746. // Merge deleted and changed items into one list
  747. $items = $deleted_items;
  748. foreach($changed_items as $changed_item)
  749. array_push($items, $changed_item["basedate"]);
  750. sort($items);
  751. // Add the merged list in to the rdata
  752. $rdata .= pack("V", count($items));
  753. foreach($items as $item)
  754. $rdata .= pack("V", $this->unixDataToRecurData($item));
  755. // Loop through the changed exceptions (not deleted)
  756. $rdata .= pack("V", count($changed_items));
  757. $items = array();
  758. foreach($changed_items as $changed_item)
  759. $items[] = $this->dayStartOf($changed_item["start"]);
  760. sort($items);
  761. // Add the changed items list int the rdata
  762. foreach($items as $item)
  763. $rdata .= pack("V", $this->unixDataToRecurData($item));
  764. // Set start date
  765. $rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["start"]));
  766. // Set enddate
  767. switch($term)
  768. {
  769. // After the given enddate
  770. case 0x21:
  771. $rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"]));
  772. break;
  773. // After a number of times
  774. case 0x22:
  775. // @todo: calculate enddate with intval($this->recur["startocc"]) + intval($this->recur["duration"]) > 24 hour
  776. $occenddate = (int) $this->recur["start"];
  777. switch ((int) $this->recur["type"]) {
  778. case 0x0A: //daily
  779. if ($this->recur["subtype"] != 1) {
  780. // -1 because the first day already counts (from 1-1-1980 to 1-1-1980 is 1 occurrence)
  781. $occenddate += (((int) $this->recur["everyn"]) * 60 * (((int) $this->recur["numoccur"] - 1)));
  782. break;
  783. }
  784. // Daily every workday
  785. $restocc = (int) $this->recur["numoccur"];
  786. // Get starting weekday
  787. $nowtime = $this->gmtime($occenddate);
  788. $j = $nowtime["tm_wday"];
  789. while (1)
  790. {
  791. if (($j % 7) > 0 && ($j % 7) < 6)
  792. $restocc--;
  793. $j++;
  794. if ($restocc <= 0)
  795. break;
  796. $occenddate += 24 * 60 * 60;
  797. }
  798. break;
  799. case 0x0B: //weekly
  800. // Needed values
  801. // $forwardcount - number of weeks we can skip forward
  802. // $restocc - number of remaning occurrences after the week skip
  803. // Add the weeks till the last item
  804. $occenddate += ($forwardcount * 7 * 24 * 60 * 60);
  805. $dayofweek = gmdate("w", $occenddate);
  806. // Loop through the last occurrences until we have had them all
  807. for ($j = 1; $restocc > 0; $j++)
  808. {
  809. // Jump to the next week (which may be N weeks away) when going over the week boundary
  810. if ((($dayofweek + $j) % 7) == $weekstart)
  811. $occenddate += (((int) $this->recur["everyn"]) - 1) * 7 * 24 * 60 * 60;
  812. // If this is a matching day, once less occurrence to process
  813. if (((int) $this->recur["weekdays"]) & (1 << (($dayofweek + $j) % 7)))
  814. $restocc--;
  815. // Next day
  816. $occenddate += 24 * 60 * 60;
  817. }
  818. break;
  819. case 0x0C: //monthly
  820. case 0x0D: //yearly
  821. $curyear = gmdate("Y", (int) $this->recur["start"] );
  822. $curmonth = gmdate("n", (int) $this->recur["start"] );
  823. // $forwardcount = months
  824. switch ((int) $this->recur["subtype"])
  825. {
  826. case 2: // on D day of every M month
  827. while ($forwardcount > 0)
  828. {
  829. $occenddate += $this->getMonthInSeconds($curyear, $curmonth);
  830. if ($curmonth >=12) {
  831. $curmonth = 1;
  832. $curyear++;
  833. } else {
  834. $curmonth++;
  835. }
  836. $forwardcount--;
  837. }
  838. // compensation between 28 and 31
  839. if (((int) $this->recur["monthday"]) >= 28 && ((int) $this->recur["monthday"]) <= 31 &&
  840. gmdate("j", $occenddate) < ((int) $this->recur["monthday"]))
  841. {
  842. if (gmdate("j", $occenddate) < 28)
  843. $occenddate -= gmdate("j", $occenddate) * 24 * 60 * 60;
  844. else
  845. $occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 * 60;
  846. }
  847. break;
  848. case 3: // on Nth weekday of every M month
  849. $nday = (int) $this->recur["nday"]; //1 tot 5
  850. $weekdays = (int) $this->recur["weekdays"];
  851. while ($forwardcount > 0)
  852. {
  853. $occenddate += $this->getMonthInSeconds($curyear, $curmonth);
  854. if ($curmonth >=12) {
  855. $curmonth = 1;
  856. $curyear++;
  857. } else {
  858. $curmonth++;
  859. }
  860. $forwardcount--;
  861. }
  862. if ($nday == 5)
  863. // Set date on the last day of the last month
  864. $occenddate += (gmdate("t", $occenddate ) - gmdate("j", $occenddate )) * 24 * 60 * 60;
  865. else
  866. // Set date on the first day of the last month
  867. $occenddate -= (gmdate("j", $occenddate )-1) * 24 * 60 * 60;
  868. for ($i = 0; $i < 7; $i++) {
  869. if ($nday == 5 && (1 << ((gmdate("w", $occenddate) - $i) % 7)) & $weekdays) {
  870. $occenddate -= $i * 24 * 60 * 60;
  871. break;
  872. } else if ($nday != 5 && (1 << ((gmdate("w", $occenddate) + $i) % 7)) & $weekdays) {
  873. $occenddate += ($i + (($nday - 1) * 7)) * 24 * 60 * 60;
  874. break;
  875. }
  876. }
  877. break; //case 3:
  878. }
  879. break;
  880. }
  881. if (defined("PHP_INT_MAX") && $occenddate > PHP_INT_MAX)
  882. $occenddate = PHP_INT_MAX;
  883. $this->recur["end"] = $occenddate;
  884. $rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"]) );
  885. break;
  886. // Never ends
  887. case 0x23:
  888. default:
  889. $this->recur["end"] = 0x7fffffff; // max date -> 2038
  890. $rdata .= pack("V", 0x5AE980DF);
  891. break;
  892. }
  893. // UTC date
  894. $utcstart = $this->toGMT($this->tz, (int) $this->recur["start"]);
  895. $utcend = $this->toGMT($this->tz, (int) $this->recur["end"]);
  896. //utc date+time
  897. $utcfirstoccstartdatetime = (isset($this->recur["startocc"])) ? $utcstart + (((int) $this->recur["startocc"])*60) : $utcstart;
  898. $utcfirstoccenddatetime = (isset($this->recur["endocc"])) ? $utcstart + (((int) $this->recur["endocc"]) * 60) : $utcstart;
  899. // update reminder time
  900. mapi_setprops($this->message, Array($this->proptags["reminder_time"] => $utcfirstoccstartdatetime ));
  901. // update first occurrence date
  902. mapi_setprops($this->message, Array($this->proptags["startdate"] => $utcfirstoccstartdatetime ));
  903. mapi_setprops($this->message, Array($this->proptags["duedate"] => $utcfirstoccenddatetime ));
  904. mapi_setprops($this->message, Array($this->proptags["commonstart"] => $utcfirstoccstartdatetime ));
  905. mapi_setprops($this->message, Array($this->proptags["commonend"] => $utcfirstoccenddatetime ));
  906. // Set Outlook properties, if it is an appointment
  907. if (isset($this->recur["message_class"]) && $this->recur["message_class"] == "IPM.Appointment") {
  908. // update real begin and real end date
  909. mapi_setprops($this->message, Array($this->proptags["startdate_recurring"] => $utcstart));
  910. mapi_setprops($this->message, Array($this->proptags["enddate_recurring"] => $utcend));
  911. // recurrencetype
  912. // Strange enough is the property recurrencetype, (type-0x9) and not the CDO recurrencetype
  913. mapi_setprops($this->message, Array($this->proptags["recurrencetype"] => ((int) $this->recur["type"]) - 0x9));
  914. // set named prop 'side_effects' to 369, needed for Outlook to ask for single or total recurrence when deleting
  915. mapi_setprops($this->message, Array($this->proptags["side_effects"] => 369));
  916. } else {
  917. mapi_setprops($this->message, Array($this->proptags["side_effects"] => 3441));
  918. }
  919. // FlagDueBy is datetime of the first reminder occurrence. Outlook gives on this time a reminder popup dialog
  920. // Any change of the recurrence (including changing and deleting exceptions) causes the flagdueby to be reset
  921. // to the 'next' occurrence; this makes sure that deleting the next ocurrence will correctly set the reminder to
  922. // the occurrence after that. The 'next' occurrence is defined as being the first occurrence that starts at moment X (server time)
  923. // with the reminder flag set.
  924. $reminderprops = mapi_getprops($this->message, array($this->proptags["reminder_minutes"]) );
  925. if(isset($reminderprops[$this->proptags["reminder_minutes"]]) ) {
  926. $occ = false;
  927. $occurrences = $this->getItems(time(), 0x7ff00000, 3, true);
  928. for($i = 0, $len = count($occurrences) ; $i < $len; $i++) {
  929. // This will actually also give us appointments that have already started, but not yet ended. Since we want the next
  930. // reminder that occurs after time(), we may have to skip the first few entries. We get 3 entries since that is the maximum
  931. // number that would be needed (assuming reminder for item X cannot be before the previous occurrence starts). Worst case:
  932. // time() is currently after start but before end of item, but reminder of next item has already passed (reminder for next item
  933. // can be DURING the previous item, eg daily allday events). In that case, the first and second items must be skipped.
  934. if(($occurrences[$i][$this->proptags["startdate"]] - $reminderprops[$this->proptags["reminder_minutes"]] * 60) > time()) {
  935. $occ = $occurrences[$i];
  936. break;
  937. }
  938. }
  939. if($occ)
  940. mapi_setprops($this->message, Array($this->proptags["flagdueby"] => $occ[$this->proptags["startdate"]] - ($reminderprops[$this->proptags["reminder_minutes"]] * 60) ));
  941. else
  942. // Last reminder passed, no reminders any more.
  943. mapi_setprops($this->message, Array($this->proptags["reminder"] => false, $this->proptags["flagdueby"] => 0x7ff00000));
  944. }
  945. // Default data
  946. // Second item (0x08) indicates the Outlook version (see documentation at the bottom of this file for more information)
  947. $rdata .= pack("VCCCC", 0x00003006, 0x08, 0x30, 0x00, 0x00);
  948. if(isset($this->recur["startocc"]) && isset($this->recur["endocc"]))
  949. // Set start and endtime in minutes
  950. $rdata .= pack("VV", (int) $this->recur["startocc"], (int) $this->recur["endocc"]);
  951. // Detailed exception data
  952. $changed_items = $this->recur["changed_occurences"];
  953. $rdata .= pack("v", count($changed_items));
  954. foreach($changed_items as $changed_item)
  955. {
  956. // Set start and end time of exception
  957. $rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
  958. $rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
  959. $rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
  960. //Bitmask
  961. $bitmask = 0;
  962. // Check for changed strings
  963. if (isset($changed_item["subject"]))
  964. $bitmask |= 1 << 0;
  965. if (isset($changed_item["remind_before"]))
  966. $bitmask |= 1 << 2;
  967. if (isset($changed_item["reminder_set"]))
  968. $bitmask |= 1 << 3;
  969. if (isset($changed_item["location"]))
  970. $bitmask |= 1 << 4;
  971. if (isset($changed_item["busystatus"]))
  972. $bitmask |= 1 << 5;
  973. if (isset($changed_item["alldayevent"]))
  974. $bitmask |= 1 << 7;
  975. if (isset($changed_item["label"]))
  976. $bitmask |= 1 << 8;
  977. $rdata .= pack("v", $bitmask);
  978. // Set "subject"
  979. if(isset($changed_item["subject"])) {
  980. // convert UTF-8 to non-unicode blob string (US-ASCII?)
  981. $subject = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["subject"]);
  982. $length = strlen($subject);
  983. $rdata .= pack("vv", $length + 1, $length);
  984. $rdata .= pack("a".$length, $subject);
  985. }
  986. if (isset($changed_item["remind_before"]))
  987. $rdata .= pack("V", $changed_item["remind_before"]);
  988. if (isset($changed_item["reminder_set"]))
  989. $rdata .= pack("V", $changed_item["reminder_set"]);
  990. if(isset($changed_item["location"])) {
  991. $location = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["location"]);
  992. $length = strlen($location);
  993. $rdata .= pack("vv", $length + 1, $length);
  994. $rdata .= pack("a".$length, $location);
  995. }
  996. if (isset($changed_item["busystatus"]))
  997. $rdata .= pack("V", $changed_item["busystatus"]);
  998. if (isset($changed_item["alldayevent"]))
  999. $rdata .= pack("V", $changed_item["alldayevent"]);
  1000. if (isset($changed_item["label"]))
  1001. $rdata .= pack("V", $changed_item["label"]);
  1002. }
  1003. $rdata .= pack("V", 0);
  1004. // write extended data
  1005. foreach($changed_items as $changed_item)
  1006. {
  1007. $rdata .= pack("V", 0);
  1008. if(isset($changed_item["subject"]) || isset($changed_item["location"])) {
  1009. $rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
  1010. $rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
  1011. $rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
  1012. }
  1013. if(isset($changed_item["subject"])) {
  1014. $subject = iconv("UTF-8", "UCS-2LE", $changed_item["subject"]);
  1015. $length = iconv_strlen($subject, "UCS-2LE");
  1016. $rdata .= pack("v", $length);
  1017. $rdata .= pack("a".$length*2, $subject);
  1018. }
  1019. if(isset($changed_item["location"])) {
  1020. $location = iconv("UTF-8", "UCS-2LE", $changed_item["location"]);
  1021. $length = iconv_strlen($location, "UCS-2LE");
  1022. $rdata .= pack("v", $length);
  1023. $rdata .= pack("a".$length*2, $location);
  1024. }
  1025. if (isset($changed_item["subject"]) || isset($changed_item["location"]))
  1026. $rdata .= pack("V", 0);
  1027. }
  1028. $rdata .= pack("V", 0);
  1029. // Set props
  1030. mapi_setprops($this->message, Array($this->proptags["recurring_data"] => $rdata, $this->proptags["recurring"] => true));
  1031. if(isset($this->tz) && $this->tz){
  1032. $timezone = "GMT";
  1033. if ($this->tz["timezone"]!=0){
  1034. // Create user readable timezone information
  1035. $timezone = sprintf("(GMT %s%02d:%02d)", (-$this->tz["timezone"]>0 ? "+" : "-"),
  1036. abs($this->tz["timezone"]/60),
  1037. abs($this->tz["timezone"]%60));
  1038. }
  1039. mapi_setprops($this->message, Array($this->proptags["timezone_data"] => $this->getTimezoneData($this->tz),
  1040. $this->proptags["timezone"] => $timezone));
  1041. }
  1042. }
  1043. /**
  1044. * Function which converts a recurrence date timestamp to an Unix date timestamp.
  1045. * @author Steve Hardy
  1046. * @param Int $rdate the date which will be converted
  1047. * @return Int the converted date
  1048. */
  1049. function recurDataToUnixData($rdate)
  1050. {
  1051. return ($rdate - 194074560) * 60 ;
  1052. }
  1053. /**
  1054. * Function which converts an Unix date timestamp to recurrence date timestamp.
  1055. * @author Johnny Biemans
  1056. * @param Date $date the date which will be converted
  1057. * @return Int the converted date in minutes
  1058. */
  1059. function unixDataToRecurData($date)
  1060. {
  1061. return ($date / 60) + 194074560;
  1062. }
  1063. /**
  1064. * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves
  1065. * @author Steve Hardy
  1066. */
  1067. function GetTZOffset($ts)
  1068. {
  1069. $Offset = date("O", $ts);
  1070. $Parity = $Offset < 0 ? -1 : 1;
  1071. $Offset = $Parity * $Offset;
  1072. $Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100;
  1073. return $Parity * $Offset;
  1074. }
  1075. /**
  1076. * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves
  1077. * @author Steve Hardy
  1078. * @param Date $time
  1079. * @return Date GMT Time
  1080. */
  1081. function gmtime($time)
  1082. {
  1083. $TZOffset = $this->GetTZOffset($time);
  1084. $t_time = $time - $TZOffset * 60; #Counter adjust for localtime()
  1085. $t_arr = localtime($t_time, 1);
  1086. return $t_arr;
  1087. }
  1088. function isLeapYear($year) {
  1089. return ( $year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0) );
  1090. }
  1091. function getMonthInSeconds($year, $month)
  1092. {
  1093. if( in_array($month, array(1,3,5,7,8,10,12) ) ) {
  1094. $day = 31;
  1095. } else if( in_array($month, array(4,6,9,11) ) ) {
  1096. $day = 30;
  1097. } else {
  1098. $day = 28;
  1099. if( $this->isLeapYear($year) == 1 )
  1100. $day++;
  1101. }
  1102. return $day * 24 * 60 * 60;
  1103. }
  1104. /**
  1105. * Function to get a date by Year Nr, Month Nr, Week Nr, Day Nr, and hour
  1106. * @param int $year
  1107. * @param int $month
  1108. * @param int $week
  1109. * @param int $day
  1110. * @param int $hour
  1111. * @return returns the timestamp of the given date, timezone-independant
  1112. */
  1113. function getDateByYearMonthWeekDayHour($year, $month, $week, $day, $hour)
  1114. {
  1115. // get first day of month
  1116. $date = gmmktime(0,0,0,$month,0,$year + 1900);
  1117. // get wday info
  1118. $gmdate = $this->gmtime($date);
  1119. $date -= $gmdate["tm_wday"] * 24 * 60 * 60; // back up to start of week
  1120. $date += $week * 7 * 24 * 60 * 60; // go to correct week nr
  1121. $date += $day * 24 * 60 * 60;
  1122. $date += $hour * 60 * 60;
  1123. $gmdate = $this->gmtime($date);
  1124. // if we are in the next month, then back up a week, because week '5' means
  1125. // 'last week of month'
  1126. if($gmdate["tm_mon"]+1 != $month)
  1127. $date -= 7 * 24 * 60 * 60;
  1128. return $date;
  1129. }
  1130. /**
  1131. * getTimezone gives the timezone offset (in minutes) of the given
  1132. * local date/time according to the given TZ info
  1133. */
  1134. function getTimezone($tz, $date)
  1135. {
  1136. // No timezone -> GMT (+0)
  1137. if(!isset($tz["timezone"]))
  1138. return 0;
  1139. $dst = false;
  1140. $gmdate = $this->gmtime($date);
  1141. $dststart = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dststartmonth"], $tz["dststartweek"], 0, $tz["dststarthour"]);
  1142. $dstend = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dstendmonth"], $tz["dstendweek"], 0, $tz["dstendhour"]);
  1143. if($dststart <= $dstend) {
  1144. // Northern hemisphere, eg DST is during Mar-Oct
  1145. if ($date > $dststart && $date < $dstend)
  1146. $dst = true;
  1147. } else {
  1148. // Southern hemisphere, eg DST is during Oct-Mar
  1149. if ($date < $dstend || $date > $dststart)
  1150. $dst = true;
  1151. }
  1152. if ($dst)
  1153. return $tz["timezone"] + $tz["timezonedst"];
  1154. else
  1155. return $tz["timezone"];
  1156. }
  1157. /**
  1158. * getWeekNr() returns the week nr of the month (ie first week of february is 1)
  1159. */
  1160. function getWeekNr($date)
  1161. {
  1162. $gmdate = gmtime($date);
  1163. $gmdate["tm_mday"] = 0;
  1164. return strftime("%W", $date) - strftime("%W", gmmktime($gmdate)) + 1;
  1165. }
  1166. /**
  1167. * parseTimezone parses the timezone as specified in named property 0x8233
  1168. * in Outlook calendar messages. Returns the timezone in minutes negative
  1169. * offset (GMT +2:00 -> -120)
  1170. */
  1171. function parseTimezone($data)
  1172. {
  1173. if(strlen($data) < 48)
  1174. return;
  1175. $tz = unpack("ltimezone/lunk/ltimezonedst/lunk/ldstendmonth/vdstendweek/vdstendhour/lunk/lunk/vunk/ldststartmonth/vdststartweek/vdststarthour/lunk/vunk", $data);
  1176. return $tz;
  1177. }
  1178. function getTimezoneData($tz)
  1179. {
  1180. $data = pack("lllllvvllvlvvlv", $tz["timezone"], 0, $tz["timezonedst"], 0, $tz["dstendmonth"], $tz["dstendweek"], $tz["dstendhour"], 0, 0, 0, $tz["dststartmonth"], $tz["dststartweek"], $tz["dststarthour"], 0 ,0);
  1181. return $data;
  1182. }
  1183. /**
  1184. * createTimezone creates the timezone as specified in the named property 0x8233
  1185. * see also parseTimezone()
  1186. * $tz is an array with the timezone data
  1187. */
  1188. function createTimezone($tz)
  1189. {
  1190. $data = pack("lxxxxlxxxxlvvxxxxxxxxxxlvvxxxxxx",
  1191. $tz["timezone"],
  1192. array_key_exists("timezonedst",$tz)?$tz["timezonedst"]:0,
  1193. array_key_exists("dstendmonth",$tz)?$tz["dstendmonth"]:0,
  1194. array_key_exists("dstendweek",$tz)?$tz["dstendweek"]:0,
  1195. array_key_exists("dstendhour",$tz)?$tz["dstendhour"]:0,
  1196. array_key_exists("dststartmonth",$tz)?$tz["dststartmonth"]:0,
  1197. array_key_exists("dststartweek",$tz)?$tz["dststartweek"]:0,
  1198. array_key_exists("dststarthour",$tz)?$tz["dststarthour"]:0
  1199. );
  1200. return $data;
  1201. }
  1202. /**
  1203. * toGMT returns a timestamp in GMT time for the time and timezone given
  1204. */
  1205. function toGMT($tz, $date) {
  1206. if(!isset($tz['timezone']))
  1207. return $date;
  1208. $offset = $this->getTimezone($tz, $date);
  1209. return $date + $offset * 60;
  1210. }
  1211. /**
  1212. * fromGMT returns a timestamp in the local timezone given from the GMT time given
  1213. */
  1214. function fromGMT($tz, $date) {
  1215. $offset = $this->getTimezone($tz, $date);
  1216. return $date - $offset * 60;
  1217. }
  1218. /**
  1219. * Function to get timestamp of the beginning of the day of the timestamp given
  1220. * @param date $date
  1221. * @return date timestamp referring to same day but at 00:00:00
  1222. */
  1223. function dayStartOf($date)
  1224. {
  1225. $time1 = $this->gmtime($date);
  1226. return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, $time1["tm_mday"], $time1["tm_year"] + 1900);
  1227. }
  1228. /**
  1229. * Function to get timestamp of the beginning of the month of the timestamp given
  1230. * @param date $date
  1231. * @return date Timestamp referring to same month but on the first day, and at 00:00:00
  1232. */
  1233. function monthStartOf($date)
  1234. {
  1235. $time1 = $this->gmtime($date);
  1236. return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, 1, $time1["tm_year"] + 1900);
  1237. }
  1238. /**
  1239. * Function to get timestamp of the beginning of the year of the timestamp given
  1240. * @param date $date
  1241. * @return date Timestamp referring to the same year but on Jan 01, at 00:00:00
  1242. */
  1243. function yearStartOf($date)
  1244. {
  1245. $time1 = $this->gmtime($date);
  1246. return gmmktime(0, 0, 0, 1, 1, $time1["tm_year"] + 1900);
  1247. }
  1248. /**
  1249. * Function which returns the items in a given interval. This included expansion of the recurrence and
  1250. * processing of exceptions (modified and deleted).
  1251. *
  1252. * @param string $entryid the entryid of the message
  1253. * @param array $props the properties of the message
  1254. * @param date $start start time of the interval (GMT)
  1255. * @param date $end end time of the interval (GMT)
  1256. */
  1257. function getItems($start, $end, $limit = 0, $remindersonly = false)
  1258. {
  1259. $items = array();
  1260. if(isset($this->recur)) {
  1261. // Optimization: remindersonly and default reminder is off; since only exceptions with reminder set will match, just look which
  1262. // exceptions are in range and have a reminder set
  1263. if($remindersonly && (!isset($this->messageprops[$this->proptags["reminder"]]) || $this->messageprops[$this->proptags["reminder"]] == false)) {
  1264. // Sort exceptions by start time
  1265. uasort($this->recur["changed_occurences"], array($this, "sortExceptionStart"));
  1266. // Loop through all changed exceptions
  1267. foreach($this->recur["changed_occurences"] as $exception) {
  1268. // Check reminder set
  1269. if(!isset($exception["reminder"]) || $exception["reminder"] == false)
  1270. continue;
  1271. // Convert to GMT
  1272. $occstart = $this->toGMT($tz, $exception["start"]);
  1273. $occend = $this->toGMT($tz, $exception["end"]);
  1274. // Check range criterium
  1275. if($occstart > $end || $occend < $start)
  1276. continue;
  1277. // OK, add to items.
  1278. array_push($items, $this->getExceptionProperties($exception));
  1279. if($limit && (count($items) == $limit))
  1280. break;
  1281. }
  1282. uasort($items, array($this, "sortStarttime"));
  1283. return $items;
  1284. }
  1285. // From here on, the dates of the occurrences are calculated in local time, so the days we're looking
  1286. // at are calculated from the local time dates of $start and $end
  1287. if ($this->recur['regen'] && isset($this->action['datecompleted']))
  1288. $daystart = $this->dayStartOf($this->action['datecompleted']);
  1289. else
  1290. $daystart = $this->dayStartOf($this->recur["start"]); // start on first day of occurrence
  1291. // Calculate the last day on which we want to be looking at a recurrence; this is either the end of the view
  1292. // or the end of the recurrence, whichever comes first
  1293. if ($end > $this->toGMT($this->tz, $this->recur["end"]))
  1294. $rangeend = $this->toGMT($this->tz, $this->recur["end"]);
  1295. else
  1296. $rangeend = $end;
  1297. $dayend = $this->dayStartOf($this->fromGMT($this->tz, $rangeend));
  1298. // Loop through the entire recurrence range of dates, and check for each occurrence whether it is in the view range.
  1299. switch($this->recur["type"])
  1300. {
  1301. case 10:
  1302. // Daily
  1303. if($this->recur["everyn"] <= 0)
  1304. $this->recur["everyn"] = 1440;
  1305. if($this->recur["subtype"] == 0) {
  1306. // Every Nth day
  1307. for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * $this->recur["everyn"])
  1308. $this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
  1309. break;
  1310. }
  1311. // Every workday
  1312. for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * 1440)
  1313. {
  1314. $nowtime = $this->gmtime($now);
  1315. if ($nowtime["tm_wday"] > 0 && $nowtime["tm_wday"] < 6) // only add items in the given timespace
  1316. $this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
  1317. }
  1318. break;
  1319. case 11:
  1320. // Weekly
  1321. if($this->recur["everyn"] <= 0)
  1322. $this->recur["everyn"] = 1;
  1323. // If sliding flag is set then move to 'n' weeks
  1324. if ($this->recur['regen']) $daystart += (60 * 60 * 24 * 7 * $this->recur["everyn"]);
  1325. for($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += (60 * 60 * 24 * 7 * $this->recur["everyn"]))
  1326. {
  1327. if ($this->recur['regen']) {
  1328. $this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
  1329. continue;
  1330. }
  1331. // Loop through the whole following week to the first occurrence of the week, add each day that is specified
  1332. for ($wday = 0; $wday < 7; $wday++)
  1333. {
  1334. $daynow = $now + $wday * 60 * 60 * 24;
  1335. //checks weather the next coming day in recurring pattern is less than or equal to end day of the recurring item
  1336. if ($daynow > $dayend)
  1337. continue;
  1338. $nowtime = $this->gmtime($daynow); // Get the weekday of the current day
  1339. if (($this->recur["weekdays"] &(1 << $nowtime["tm_wday"]))) // Selected ?
  1340. $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
  1341. }
  1342. }
  1343. break;
  1344. case 12:
  1345. // Monthly
  1346. if($this->recur["everyn"] <= 0)
  1347. $this->recur["everyn"] = 1;
  1348. // Loop through all months from start to end of occurrence, starting at beginning of first month
  1349. for($now = $this->monthStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60 )
  1350. {
  1351. if(isset($this->recur["monthday"]) &&($this->recur['monthday'] != "undefined") && !$this->recur['regen']) { // Day M of every N months
  1352. $difference = 1;
  1353. if ($this->daysInMonth($now, $this->recur["everyn"]) < $this->recur["monthday"])
  1354. $difference = $this->recur["monthday"] - $this->daysInMonth($now, $this->recur["everyn"]) + 1;
  1355. $daynow = $now + (($this->recur["monthday"] - $difference) * 24 * 60 * 60);
  1356. //checks weather the next coming day in recurrence pattern is less than or equal to end day of the recurring item
  1357. if ($daynow <= $dayend)
  1358. $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
  1359. }
  1360. else if(isset($this->recur["nday"]) && isset($this->recur["weekdays"])) { // Nth [weekday] of every N months
  1361. // Sanitize input
  1362. if($this->recur["weekdays"] == 0)
  1363. $this->recur["weekdays"] = 1;
  1364. // If nday is not set to the last day in the month
  1365. if ($this->recur["nday"] < 5) {
  1366. // keep the track of no. of time correct selection pattern(like 2nd weekday, 4th fiday, etc.)is matched
  1367. $ndaycounter = 0;
  1368. // Find matching weekday in this month
  1369. for($day = 0; $day < $this->daysInMonth($now, 1); $day++)
  1370. {
  1371. $daynow = $now + $day * 60 * 60 * 24;
  1372. $nowtime = $this->gmtime($daynow); // Get the weekday of the current day
  1373. if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) // Selected ?
  1374. $ndaycounter ++;
  1375. // check the selected pattern is same as asked Nth weekday,If so set the firstday
  1376. if($this->recur["nday"] == $ndaycounter){
  1377. $firstday = $day;
  1378. break;
  1379. }
  1380. }
  1381. // $firstday is the day of the month on which the asked pattern of nth weekday matches
  1382. $daynow = $now + $firstday * 60 * 60 * 24;
  1383. }else{
  1384. // Find last day in the month ($now is the firstday of the month)
  1385. $NumDaysInMonth = $this->daysInMonth($now, 1);
  1386. $daynow = $now + (($NumDaysInMonth-1) * 24*60*60);
  1387. $nowtime = $this->gmtime($daynow);
  1388. while (($this->recur["weekdays"] & (1 << $nowtime["tm_wday"]))==0){
  1389. $daynow -= 86400;
  1390. $nowtime = $this->gmtime($daynow);
  1391. }
  1392. }
  1393. /**
  1394. * checks weather the next coming day in recurrence pattern is less than or equal to end day of the * recurring item.Also check weather the coming day in recurrence pattern is greater than or equal to start * of recurring pattern, so that appointment that fall under the recurrence range are only displayed.
  1395. */
  1396. if ($daynow <= $dayend && $daynow >= $daystart)
  1397. $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz , $remindersonly);
  1398. } else if ($this->recur['regen']) {
  1399. $next_month_start = $now + ($this->daysInMonth($now, 1) * 24 * 60 * 60);
  1400. $now = $daystart +($this->daysInMonth($next_month_start, $this->recur['everyn']) * 24 * 60 * 60);
  1401. if ($now <= $dayend)
  1402. $this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
  1403. }
  1404. }
  1405. break;
  1406. case 13:
  1407. // Yearly
  1408. if($this->recur["everyn"] <= 0)
  1409. $this->recur["everyn"] = 12;
  1410. for($now = $this->yearStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60 )
  1411. {
  1412. if(isset($this->recur["monthday"]) && !$this->recur['regen']) { // same as monthly, but in a specific month
  1413. // recur["month"] is in minutes since the beginning of the year
  1414. $month = $this->monthOfYear($this->recur["month"]); // $month is now month of year [0..11]
  1415. $monthday = $this->recur["monthday"]; // $monthday is day of the month [1..31]
  1416. $monthstart = $now + $this->daysInMonth($now, $month) * 24 * 60 * 60; // $monthstart is the timestamp of the beginning of the month
  1417. if($monthday > $this->daysInMonth($monthstart, 1))
  1418. $monthday = $this->daysInMonth($monthstart, 1); // Cap $monthday on month length (eg 28 feb instead of 29 feb)
  1419. $daynow = $monthstart + ($monthday-1) * 24 * 60 * 60;
  1420. $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
  1421. }
  1422. else if(isset($this->recur["nday"]) && isset($this->recur["weekdays"])) { // Nth [weekday] in month X of every N years
  1423. // Go the correct month
  1424. $monthnow = $now + $this->daysInMonth($now, $this->monthOfYear($this->recur["month"])) * 24 * 60 * 60;
  1425. // Find first matching weekday in this month
  1426. for($wday = 0; $wday < 7; $wday++)
  1427. {
  1428. $daynow = $monthnow + $wday * 60 * 60 * 24;
  1429. $nowtime = $this->gmtime($daynow); // Get the weekday of the current day
  1430. if($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
  1431. $firstday = $wday;
  1432. break;
  1433. }
  1434. }
  1435. // Same as above (monthly)
  1436. $daynow = $monthnow + ($firstday + ($this->recur["nday"]-1)*7) * 60 * 60 * 24;
  1437. while ($this->monthStartOf($daynow) != $this->monthStartOf($monthnow))
  1438. $daynow -= 7 * 60 * 60 * 24;
  1439. $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
  1440. } else if ($this->recur['regen']) {
  1441. $year_starttime = $this->gmtime($now);
  1442. $is_next_leapyear = $this->isLeapYear($year_starttime['tm_year'] + 1900 + 1); // +1 next year
  1443. $now = $daystart + ($is_next_leapyear ? 31622400 /* Leap year in seconds */ : 31536000 /*year in seconds*/);
  1444. if ($now <= $dayend)
  1445. $this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
  1446. }
  1447. }
  1448. }
  1449. //to get all exception items
  1450. if (!empty($this->recur['changed_occurences']))
  1451. $this->processExceptionItems($items, $start, $end);
  1452. }
  1453. // sort items on starttime
  1454. usort($items, array($this, "sortStarttime"));
  1455. // Return the MAPI-compatible list of items for this object
  1456. return $items;
  1457. }
  1458. function sortStarttime($a, $b)
  1459. {
  1460. $aTime = $a[$this->proptags["startdate"]];
  1461. $bTime = $b[$this->proptags["startdate"]];
  1462. return $aTime==$bTime?0:($aTime>$bTime?1:-1);
  1463. }
  1464. /**
  1465. * daysInMonth
  1466. *
  1467. * Returns the number of days in the upcoming number of months. If you specify 1 month as
  1468. * $months it will give you the number of days in the month of $date. If you specify more it
  1469. * will also count the days in the upcomming months and add that to the number of days. So
  1470. * if you have a date in march and you specify $months as 2 it will return 61.
  1471. * @param Integer $date Specified date as timestamp from which you want to know the number
  1472. * of days in the month.
  1473. * @param Integer $months Number of months you want to know the number of days in.
  1474. * @returns Integer Number of days in the specified amount of months.
  1475. */
  1476. function daysInMonth($date, $months) {
  1477. $days = 0;
  1478. for ($i = 0; $i < $months; $i++)
  1479. $days += date("t", $date + $days * 24 * 60 * 60);
  1480. return $days;
  1481. }
  1482. // Converts MAPI-style 'minutes' into the month of the year [0..11]
  1483. function monthOfYear($minutes) {
  1484. $d = gmmktime(0,0,0,1,1,2001); // The year 2001 was a non-leap year, and the minutes provided are always in non-leap-year-minutes
  1485. $d += $minutes*60;
  1486. $dtime = $this->gmtime($d);
  1487. return $dtime["tm_mon"];
  1488. }
  1489. function sortExceptionStart($a, $b)
  1490. {
  1491. return $a["start"] == $b["start"] ? 0 : ($a["start"] > $b["start"] ? 1 : -1 );
  1492. }
  1493. }
  1494. ?>