PageRenderTime 34ms CodeModel.GetById 34ms RepoModel.GetById 1ms app.codeStats 0ms

/backend/zarafa/mapi/class.baserecurrence.php

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