From 121652b06b516af77a95fa3b18f62975c20b17e1 Mon Sep 17 00:00:00 2001 From: "Michael J. Rubinsky" Date: Fri, 26 Mar 2010 14:06:41 -0400 Subject: [PATCH] Initial import of ActiveSync support into master branch. This should go without saying, but: This code is very experimental at the moment. It is functional in my tests, but there is sure to be many bugs still to be worked out. If you use activesync support at this point, it may work, it may not, it may make your iPod grow legs and run for high ground. You have been warned ;) Will put up some documentation, TODOs, issues etc... as I have time on the wiki project page at http://wiki.horde.org/Project/ActiveSync Squashed commit of the following: commit d8d6dafb925503e328b3688cec6422ab130bd77c Author: Michael J. Rubinsky Date: Fri Mar 26 13:45:47 2010 -0400 remove commented out code commit f790aa31c91e6c0747dc2b50e26c59755ec71cf4 Author: Michael J. Rubinsky Date: Fri Mar 26 13:44:25 2010 -0400 add a bare-bones config tab for activesync. Right now, it just enables/disables as sets the directory for the state files commit aef0cf5654ea5b0daa75202fcae0edee95701221 Merge: 54bee83 8e71015 Author: Michael J. Rubinsky Date: Fri Mar 26 13:16:24 2010 -0400 Merge branch 'master' into ActiveSync-refactor1 commit 54bee830a1df9bfa219e642a8492377a8507295c Author: Michael J. Rubinsky Date: Fri Mar 26 13:13:05 2010 -0400 Eh, turns out that SYNC_POOMCAL_EXCEPTIONSTARTTIME is actually supposed to hold the *original* start time of the recurring event, not the exception's start time. THAT goes in the SYNC_POOMCAL_STARTTIME field, just like a normal appointment. Thank you, MS. commit 2dc95253f0fb2dbaa859c9dba4230df441cf9233 Author: Michael J. Rubinsky Date: Fri Mar 26 13:11:08 2010 -0400 Changes to fix timezone handling again. All dates are stored intenrally by the message object as Horde_Date objects in the local tz. The encoder will convert to UTC before putting it out to the pim. Likewise, the decoder creates Horde_Date objects in the local tz from the UTC time passed to us from the PIM. commit 4866f716d446f10bb93722a016f9bf9c6c102e2d Author: Michael J. Rubinsky Date: Fri Mar 26 13:09:40 2010 -0400 wrap commit 2460101269881807de4265eae1330dea992e8357 Author: Michael J. Rubinsky Date: Fri Mar 26 13:08:35 2010 -0400 return the baseid as well commit 8ecc8836c4282a3fc5779a53661e21ba745d596e Author: Michael J. Rubinsky Date: Fri Mar 26 10:53:54 2010 -0400 Start making the activesync message objects more like a content type mapper. Start providing a similar api for setting/getting properties from these types of objects. This commit completely rewrites the Appointment type, along with the supporting code in kronolith to convert an event from/to an activesync appointment. Still some issues with recurrence exceptions to work out. commit c6a871cc3e00b2e8f12b64e90fed53728d73580b Author: Michael J. Rubinsky Date: Fri Mar 26 10:51:48 2010 -0400 Add wbxml mapping for POOMCAL:ResponseType commit 1f742938b6b0049985970fb26d23a2faeffa33b4 Author: Michael J. Rubinsky Date: Fri Mar 26 10:50:10 2010 -0400 give this a default value commit dec6bf7e24ab03603bfb50a96ecd8186efd0ff7b Author: Michael J. Rubinsky Date: Thu Mar 25 12:37:32 2010 -0400 Allow filtering out events that represent exceptions to recurring events. commit 7a0ba71f1a48016516912015bce3c36e72448ff7 Author: Michael J. Rubinsky Date: Thu Mar 25 11:07:57 2010 -0400 Add a baseid property to events. Used for mapping an event that represents an exception to the original, recurring event. Needed for dealing with exceptions when syncing with ActiveSync devices (and possibly others). commit 9585333db3e219047790929f446048821a3695a2 Author: Michael J. Rubinsky Date: Wed Mar 24 20:41:00 2010 -0400 Fix default value for image_faces value in ansel_images table commit dca8d32e8925e5e5afdead1bb1e54e9d2107853d Author: Michael J. Rubinsky Date: Thu Mar 18 18:44:06 2010 -0400 Fix PROVISION command handling commit f11c04a5a25e9b221c65c06a2e039f2e3f8626c3 Author: Michael J. Rubinsky Date: Wed Mar 17 18:01:50 2010 -0400 remove commented out, obsolete code commit 0685902176b2bdf3d719ae2bc4ec0249f136d6a6 Author: Michael J. Rubinsky Date: Wed Mar 17 12:47:53 2010 -0400 Remove obsolete files commit 8d099d9d52178da2cc38f939a4cfa27d69711c76 Author: Michael J. Rubinsky Date: Wed Mar 17 12:46:16 2010 -0400 Add Horde_ActiveSync_Message objects, remove Horde_ActiveSync_Streamer_* objects. Also, phpdoc, add encodeStream() and decodeStream() to allow encode() and decode() to take strings. commit f5bda8a80b6603ca6b044c3a2cee6d0454fdbcce Author: Michael J. Rubinsky Date: Mon Mar 15 15:58:15 2010 -0400 Add mostly un-implemented methods for working with a history-based state driver commit b1a2ecb358c8e314b7d50e9491b106a3038a99d8 Author: Michael J. Rubinsky Date: Mon Mar 15 15:57:46 2010 -0400 Add completely un-working History driver commit 1686fbef629baa9025522aed594de043db2e1234 Author: Michael J. Rubinsky Date: Mon Mar 15 15:57:28 2010 -0400 ws commit 071e8af3fb25d1b0e67b3bc06d49198fbaa7ed80 Author: Michael J. Rubinsky Date: Mon Mar 15 15:57:01 2010 -0400 Fix usage of handle() commit a142183e8fcbf573fa7e2d2cc0d58b874ce5f2d2 Author: Michael J. Rubinsky Date: Mon Mar 15 15:56:22 2010 -0400 remove obsolete code commit e40a56caf42e0a96e4d1bb3c17e19dd308279a2b Author: Michael J. Rubinsky Date: Mon Mar 15 15:52:26 2010 -0400 Add recurring event support Still needs work - New recurring events are added currectly in both directions but editing exceptions do not yet work correctly. Edited on PIM -> Server - The server correctly receives the exception, and stores it, but no new event is created for the occurance of the exception. Edited on Server -> PIM - A new event is sent to the PIM for the exception, but the existing occurance is not removed. commit 82a5c429bd25107c6388fcb092ef8f85a7f13e82 Author: Michael J. Rubinsky Date: Mon Mar 15 15:47:27 2010 -0400 stub out a test to add for a bug that just wasted an hour of my life commit 8ea8ae99c30a018a9c57c69f78af8e0324d8742a Author: Michael J. Rubinsky Date: Mon Mar 15 15:46:11 2010 -0400 Don't forget to update the PingState if this is a PING request. commit af8a62a35933c9700cd8bd434aa24a537299a2a0 Author: Michael J. Rubinsky Date: Mon Mar 15 13:00:53 2010 -0400 Make Kronolith#listEvents honor the $showRecurrence parameter commit 4129be1a2517948a3f38437af6416cb57295f29f Author: Michael J. Rubinsky Date: Mon Mar 15 09:58:20 2010 -0400 Move setNewSyncKey to the base class, other tweaks commit 1bd2b1b7bcb32b9421954b39485083b38e8d2166 Author: Michael J. Rubinsky Date: Mon Mar 15 09:57:24 2010 -0400 ws commit 4656c1951d3e7869decdbb67718efde248730f89 Author: Michael J. Rubinsky Date: Mon Mar 15 09:34:21 2010 -0400 ws commit 609344e8bb2cd546c2f64b045e51673c4fa2a8c0 Author: Michael J. Rubinsky Date: Mon Mar 15 09:32:50 2010 -0400 Notes as I distill more about the AS protocol versions commit f8a2f1060f4681c73def8848af13b38e9eb6392c Author: Michael J. Rubinsky Date: Sun Mar 14 11:17:28 2010 -0400 Move PROVISION command handling to Request_Provision commit 77371eda5feef7cb0416cb36db689cdcd47fb877 Author: Michael J. Rubinsky Date: Sun Mar 14 09:44:02 2010 -0400 Remove code that was moved to individual Request classes commit 3b4fafb72de8ea1b04ee8b6847351b1bfbfaf8c8 Author: Michael J. Rubinsky Date: Sun Mar 14 09:41:04 2010 -0400 Fix foldersync state handling Better performance, one less state file to worry about. commit ee983e0f4e91fdb80e05a1fdd4205627c6b56242 Author: Michael J. Rubinsky Date: Sat Mar 13 16:41:57 2010 -0500 No need to loadState for FolderSync requests, the getKnownFolders method takes care of it. commit 37611701f37ed94e9aff3875b4b1aa2ecade78fb Author: Michael J. Rubinsky Date: Sat Mar 13 16:30:34 2010 -0500 phpdoc commit 61e0062ab353e76eac595058fcdfb532d73a763e Author: Michael J. Rubinsky Date: Sat Mar 13 16:30:04 2010 -0500 Fix isConflict() usage commit cc2f3b5c2ace883b845248d348aaee99e2b21181 Author: Michael J. Rubinsky Date: Sat Mar 13 16:26:55 2010 -0500 phpdoc, style commit d2e846e48b7cf673ba8e3b0252fa55f14879fd64 Author: Michael J. Rubinsky Date: Sat Mar 13 14:34:48 2010 -0500 Remove the last(?) of the obsolted files commit cba4d4a98c9b9b104f51b6544456e2b65b806985 Author: Michael J. Rubinsky Date: Sat Mar 13 12:04:49 2010 -0500 Remove deprecated files commit 86f9cb62aa9e3a2b19347014330cf214eb180a20 Author: Michael J. Rubinsky Date: Fri Mar 12 19:37:42 2010 -0500 Remove unused paramter commit 3899f8f916df031b2bdd333b9426780b726db8e2 Author: Michael J. Rubinsky Date: Fri Mar 12 19:37:18 2010 -0500 Remove this hack, and implement it correctly. commit 17b3e4696cbc78668ddf71ec505e6eebabb2e04a Author: Michael J. Rubinsky Date: Fri Mar 12 19:36:01 2010 -0500 rename loadPingState to loadPingCollectionState to better describe it ... also differentiate it from initPingState() commit 42f29077c2d543ff8789eb853137b1279a43f565 Author: Michael J. Rubinsky Date: Fri Mar 12 19:34:49 2010 -0500 phpdoc commit 0c1a971c560a4d8d311b54e4949d3d0ca0aa7831 Author: Michael J. Rubinsky Date: Fri Mar 12 19:34:16 2010 -0500 Fix usage of state->updateState() commit 7991a18fa51122a433aae446008431b3eb1fe553 Author: Michael J. Rubinsky Date: Fri Mar 12 19:33:43 2010 -0500 remove deprecated code commit 9875dd689ea1c0c97e8754e56530456789f58110 Author: Michael J. Rubinsky Date: Fri Mar 12 17:41:45 2010 -0500 stub out other needed tests commit b378bc1bee1a6fb176ed3e0f728e9c552d8a350d Author: Michael J. Rubinsky Date: Fri Mar 12 17:37:12 2010 -0500 Move more constants to class constants commit 36dbb076054e88e2bca99b89cc06b527db5215a5 Author: Michael J. Rubinsky Date: Fri Mar 12 17:36:51 2010 -0500 Ensure required parameters are available commit 25cb4f9a32006bd356b9eff5fdc023db42c5119d Author: Michael J. Rubinsky Date: Fri Mar 12 17:36:23 2010 -0500 ws commit 822ae27373913aa9a9651d34afb345c672af2b5d Author: Michael J. Rubinsky Date: Fri Mar 12 17:33:55 2010 -0500 Start adding tests for the file based state machine commit 2786a9ae1035dd608a433fbf96c1943e45e3ba44 Author: Michael J. Rubinsky Date: Thu Mar 11 14:54:52 2010 -0500 Remove obsolete parameter commit d3ac1ae688ab5ec2af4cfa334bff543e7eccf2cc Author: Michael J. Rubinsky Date: Thu Mar 11 14:12:32 2010 -0500 Properly handle OPTIONS request commit 82b6a6a86743894aced0bff82803fdf92f0b3f9e Author: Michael J. Rubinsky Date: Thu Mar 11 14:11:39 2010 -0500 Fix state handling for PING requests commit 66f37afb69450e5e5b08cfa2165228b69bfed078 Author: Michael J. Rubinsky Date: Thu Mar 11 14:10:52 2010 -0500 remove commented code commit 44e687e0ccc333f6e51401941e9a444f85fb8b67 Author: Michael J. Rubinsky Date: Thu Mar 11 12:16:09 2010 -0500 Start breaking out the logic for handling requests into individual classes. The 'major' requests are complete and working. Left the requests that we don't currently use in the Active_Sync class so we still have the code, but it's not currently used. Will move those out as they are implemented/tested. Likewise, removed deprecated classes from package.xml, but for now, have left the files until everything is complete. commit c95948d87e64b7d12cb2f20f376b4556e99d3374 Author: Michael J. Rubinsky Date: Thu Mar 11 00:39:46 2010 -0500 Check for required parameters, remove old cruft commit 0b66004e4ba90b24251a7fe7ab86d7decfa32e74 Author: Michael J. Rubinsky Date: Thu Mar 11 00:39:19 2010 -0500 update test commit d5e9e1b05c8b3a08844988b906dc315f06cf199d Author: Michael J. Rubinsky Date: Thu Mar 11 00:28:45 2010 -0500 Document the params commit e42269d2420085c3a8204eca7532f61e5f62e8a4 Author: Michael J. Rubinsky Date: Thu Mar 11 00:01:08 2010 -0500 First round of *massive* refactoring This starts moving responsibility for various things around to facilitate each storage backend to decide what state storage to use. Moves the majority of the functionality that would change to the new State classes. Lots of cruft still left here, some of the older state classes will go away, along with some of the importer and exporter classes...but this is currently working again for contacts and calendars. Committing to get a touchstone for a working state. commit 51392010d1474909ddf08c24348d51a4a9d26155 Author: Michael J. Rubinsky Date: Wed Mar 3 15:52:52 2010 -0500 Initial commit for new State handler commit 6a85f9ca827fa77931c950d480f4ca3ba38e02bb Author: Michael J. Rubinsky Date: Mon Feb 22 17:36:50 2010 -0500 base implementation for getState() and setState() commit fbe7d2db78e705072d2341799066a091ff30a282 Author: Michael J. Rubinsky Date: Mon Feb 22 17:11:42 2010 -0500 First stage in refactoring state Rename getState() to loadState(), and add an (as yet unimplemented) method getState() which will return the in-memory state. Also rename setState to saveState() which saves the in-memory state to storage and re-add a setState() that will only set the in-memory state. commit 1e8046846448e22ae808c73583a5fff4ca6bbd1e Author: Michael J. Rubinsky Date: Sun Feb 21 18:25:53 2010 -0500 This is actually SYNC_COMMANDS, not SYNC_PERFORM Try to keep the code page definitions in line with MS docs commit 81de70b3be4976ac834e5d1c2e681111078234d0 Author: Michael J. Rubinsky Date: Sun Feb 21 10:41:00 2010 -0500 Finish proper error handling in HandleSync commit 3ef31a79eb770fa74b469b9d2af4c99ad2f55116 Author: Michael J. Rubinsky Date: Sun Feb 21 10:33:20 2010 -0500 Fix reveresed logic commit cb537103653fb39f541b45b06cd15649d5278036 Author: Michael J. Rubinsky Date: Fri Feb 12 20:20:16 2010 -0500 This is incorrect. A value of 0x15 on the AirSync codepage is WINDOWSIZE, not MAXITEMS. MAXITEMS is only used when dealing with the recipient resolver cache, not with SYNC requests. This explains what I *though* was illegial client behaviour, but it was jsut incorrect nomenclature. commit b76cd952f5dc763a4b78138a6d3ac1faaddfc566 Author: Michael J. Rubinsky Date: Fri Feb 12 20:03:47 2010 -0500 enforce the protocol rule that SUPPORTED tags are only allowed on inital sync (synckey == 0) commit d39cad04fd744389cc88f1a363029b88f4327571 Author: Michael J. Rubinsky Date: Fri Feb 12 20:01:12 2010 -0500 Start fleshing out support for properly dealing with errors commit 1cbea9bbea44a695b11ad2b3ff3c9d42c0e9743a Author: Michael Rubinsky Date: Fri Feb 12 19:16:51 2010 -0500 Fix category support commit 47a8162cde1dfc824f3d2276c404f557ecddcf50 Author: Michael J. Rubinsky Date: Fri Feb 12 18:36:11 2010 -0500 First attempt at Categories support commit 2b386986c4c60e5ee78178cac933136141eb35fe Author: Michael J. Rubinsky Date: Fri Feb 12 18:10:08 2010 -0500 Fill in missing contact attributes .. just 'children' and 'catagories' are missing now commit 705608f9b7d6a3bde12a875e81d7dfceb008ef6d Author: Michael J. Rubinsky Date: Thu Feb 11 21:02:31 2010 -0500 Add anniversary field commit fe3372592d371a639767c6c77c097234d34d9ad0 Author: Michael J. Rubinsky Date: Thu Feb 11 21:01:59 2010 -0500 Better test coverage commit b25e2356bfeb9fd0fe349baac368e64479f3a5c5 Author: Michael J. Rubinsky Date: Thu Feb 11 20:02:38 2010 -0500 fix property name commit 6cc2cda15b7fbb6efecee39cc4cd76f055631e23 Author: Michael J. Rubinsky Date: Thu Feb 11 18:31:18 2010 -0500 Enable contact picture syncing. Works, but has issues on Android. Android will receive the pitcure information fine, but does not SEND the picture data at all. Worse yet, the SYNC_POOMCONTACTS_PICTURE is not marked as a ghostable element...but even if it was, Android does not send any SYNC_SUPPORTED information during the initial sync. The bottom line is, for now, that picture data will be erased on the server anytime a contact with a picture has changes that are transmitted from Android -> Server. commit 053685a1fb91a654a35ee1a802f8a9e52a088e47 Author: Michael J. Rubinsky Date: Thu Feb 11 18:26:10 2010 -0500 cs, comment cleanup commit ec775a90122a396164b1f7f2be9eefbf1669690f Author: Michael J. Rubinsky Date: Wed Feb 10 17:30:37 2010 -0500 Don't get all occurances of recurring events commit 1b79891794e9acd8e05e692b2f0b4b88a2c98a08 Author: Michael J. Rubinsky Date: Wed Feb 10 16:06:29 2010 -0500 Fix adding events via the API. Sneaky little bug. Can't catch a Kronolith_Exception here, since Kronolith_Driver#getByUID can also throw a Horde_Exception_NotFound now too. commit 6b9b4482073700588482fa23d78f0a254dbf459d Author: Michael J. Rubinsky Date: Wed Feb 10 14:37:54 2010 -0500 Fix variable name commit 939127c4812cba6d20f9e6fa8202151004de0909 Author: Michael J. Rubinsky Date: Wed Feb 10 11:58:50 2010 -0500 Add timezone support to calendar data. Create the activesync TIMEZONE structure based on the user's timezone. Events now properly show the correct time on both PIM and Server regardless of what timezone either is set to. Recurring event support to follow commit a3f493455b98c5e0649c24bf0348adea0b756a09 Author: Michael J. Rubinsky Date: Wed Feb 10 11:15:47 2010 -0500 Don't get the policy key if we are not provisioning commit 97b3e101f87f814529dd830b804732815a07b3b2 Author: Michael J. Rubinsky Date: Wed Feb 10 11:14:25 2010 -0500 ws, cs, etc... commit 5e7eb678e37381a0fc4d64e749cdc5a79718526e Author: Michael J. Rubinsky Date: Wed Feb 10 01:16:20 2010 -0500 Timezone utilities for dealing with ActiveSync timezone structures along with unit tests commit fceac1de1d699d2ff9999d88831b7d46c870afd1 Author: Michael J. Rubinsky Date: Tue Feb 9 13:05:51 2010 -0500 Explain this test, check for the exception and fail() if caught. commit 4391d58b4caabfae6688e3b974c334c8ec22ffa2 Author: Michael J. Rubinsky Date: Tue Feb 9 13:02:47 2010 -0500 add test to check for proper charset in streamer objects commit 2bc6fdc478896fce4e65b64427acec5fd1b43070 Author: Michael J. Rubinsky Date: Tue Feb 9 12:43:45 2010 -0500 Fix hash key errors caught by unit tests commit 400721b219e38ec0b21bcbbb5fc4284ed1f301d5 Author: Michael J. Rubinsky Date: Tue Feb 9 12:42:07 2010 -0500 Update tests to track changes in the Horde driver commit ff6a7b02c0ff43def766bb2ef2d3849f40568ce9 Author: Michael J. Rubinsky Date: Tue Feb 9 12:29:59 2010 -0500 fix bug caught by unit test commit c74245817b445c42817aa64f9474a0c5ae77aa31 Author: Michael J. Rubinsky Date: Sat Feb 6 17:25:05 2010 -0500 Don't expect Horde_Rpc to know any details about the ActiveSync protocol commit ed4088be1a3d1c4044bff4cbb8e7c087515d12dc Author: Michael J. Rubinsky Date: Sat Feb 6 16:07:04 2010 -0500 Track provisioning changes in Rpc commit c47af26a42577bfef7e4946304ff1f9123ad8b2e Author: Michael J. Rubinsky Date: Sat Feb 6 16:05:51 2010 -0500 Add setter for changing the provisioning requirement commit 76f59e9cc1d1029f071f1c52fac443260e3cbf62 Author: Michael J. Rubinsky Date: Sat Feb 6 15:47:08 2010 -0500 Start implementing PROVISION support. Basic PROVISION support is functional, though it doesn't actually check the validity of policy keys yet. This will be fixed once state storage is refactored. Also need to add support for configuring the security policies sent. Right now, just a general, very loose security policy is sent. Tested with the TouchDown client application on Android 2.0.1 since this is the only client application I have access to that performs provisioning commit 51b5c34afe556c788f60f75602c9a135109e0f73 Author: Michael J. Rubinsky Date: Sat Feb 6 01:31:11 2010 -0500 Don't create Horde_Date if we don't have a date commit 8935865aeb996c0bebba4bd4011ace97b9b88e14 Author: Michael J. Rubinsky Date: Sat Feb 6 01:06:25 2010 -0500 Don't waste cycles creating a vCard just to convert it to something else The vCard gives us a nice, known set of attribute names, but is very wastful in this case. Just get the attibutes list from contacts/export and convert directly to the Streamer object we need...and vice versa commit 53cfa34a34776a8d1e203615c386d42d61ec81b7 Author: Michael J. Rubinsky Date: Sat Feb 6 01:05:30 2010 -0500 Add support for returning an attributes hash from contacts/export commit 19dd45e455a07b03e84d8c00ccb79a498170e109 Author: Michael J. Rubinsky Date: Sat Feb 6 01:02:39 2010 -0500 check for failure commit 0f2e9cbd43df81b59a42f20e3fb35d5875cb1eac Author: Michael J. Rubinsky Date: Fri Feb 5 11:42:47 2010 -0500 Don't attempt to import a birthday value if there is no value set commit 346ecf539771e924ab62e0795ae20605130d0f2d Author: Michael J. Rubinsky Date: Fri Feb 5 10:18:32 2010 -0500 Fix layout of File_Csv pacakge commit 6410dcfe70207ca8c231d9aef141bffb35323c8c Author: Michael J. Rubinsky Date: Wed Feb 3 18:21:47 2010 -0500 Allow indicating if we want remote/listTimeObject events from calendar/listEvents commit 9800e2e8cfa55a77316000ed4366fdca4d1b3a59 Author: Michael J. Rubinsky Date: Wed Feb 3 20:20:30 2010 -0500 Don't request remote or listTimeObject events for activesync requests commit 040bee3366e14281ee44fb5f3ac5b38c1fbb0842 Author: Michael J. Rubinsky Date: Wed Feb 3 13:03:53 2010 -0500 Don't assume we always have a maxitems tag commit d69ac2683c56dd5c526d21d6b4faae6b42c15091 Author: Michael J. Rubinsky Date: Tue Feb 2 20:39:48 2010 -0500 ws commit a3fd012cb0e237bd4e99a5e53c040b97295a8477 Author: Michael J. Rubinsky Date: Tue Feb 2 11:27:13 2010 -0500 Add default value to parameter Fixes warnings and notices from the factory commit e7da02d9c9eac4864b5be1fb3d161814ecb4e552 Author: Michael J. Rubinsky Date: Tue Feb 2 11:25:04 2010 -0500 Fix file name commit b813844132ce6162137dd81e94ebe2192be23a2e Author: Michael J. Rubinsky Date: Tue Feb 2 10:41:23 2010 -0500 Comment out the sleep() call again for now, still seems to cause server hangs and other issues with buffered output commit 58c85f83d1cb40121b121a24471fa13a0212b6a4 Author: Michael J. Rubinsky Date: Tue Feb 2 10:40:43 2010 -0500 Add __get/__set/__isset magic methods to base class commit 45625ebdb54c29a6c2ca5751ce30b0b2377f337a Author: Michael J. Rubinsky Date: Tue Feb 2 10:39:58 2010 -0500 Add birthday field support to contacts commit eb6217f67a8bf418255bff904c16a222949a202f Author: Michael J. Rubinsky Date: Tue Feb 2 10:38:51 2010 -0500 Add PIM -> Server event creation support commit 8e80ffebde1bf948c09926c28d31f3162177fcae Author: Michael J. Rubinsky Date: Mon Feb 1 15:11:32 2010 -0500 Remove apparently unused memebers commit 5194ef414846879d9711e8663ef20d54b24a5e50 Author: Michael J. Rubinsky Date: Mon Feb 1 14:15:07 2010 -0500 typos commit 96d610cc8392d4ecceaa3674700f74280503a15c Author: Michael J. Rubinsky Date: Mon Feb 1 14:14:15 2010 -0500 Add vCalendar parsing for server->pim commit 83256317edc12b76448d4de476c3f5a5cc3ff9eb Author: Michael J. Rubinsky Date: Mon Feb 1 14:13:57 2010 -0500 fix api method name commit 534d832b115360c71191ed6337ab0c52ed94ce62 Author: Michael J. Rubinsky Date: Mon Feb 1 14:13:04 2010 -0500 cs commit 913c9255d432b6de4669434fa4ffde4cb2475de4 Author: Michael J. Rubinsky Date: Mon Feb 1 14:12:05 2010 -0500 reenable the sleep call, now that the rest of this method is working commit 39ee598fbf17e6a6faa8507f1c4a338dae0261f8 Author: Michael J. Rubinsky Date: Mon Feb 1 11:06:53 2010 -0500 (re)add missing ChunkContent ajax action commit 53b53c3692052ba2fc6a9ea28d48ff86de0214cd Author: Michael J. Rubinsky Date: Mon Feb 1 10:16:48 2010 -0500 clean up log entries, add notes/todos before I forget them commit a325fc785c66fea0869d66fb19093c5ca7677245 Author: Michael J. Rubinsky Date: Mon Feb 1 09:41:19 2010 -0500 Fix case of method names to horde cs standards commit 0085e52b83fdd725f02f507cb16aeed5e6a5dc45 Author: Michael J. Rubinsky Date: Mon Feb 1 09:39:20 2010 -0500 Remove this dead code for now, will revist if/when imap support is added commit cd163a69fc7a97fb9d19d1645e27bdb723e97388 Author: Michael J. Rubinsky Date: Mon Feb 1 09:38:54 2010 -0500 phpdoc commit a670e3b6cb946f15f5f3e07d02de1eb95da964b9 Author: Michael J. Rubinsky Date: Mon Feb 1 09:33:01 2010 -0500 Create the dependencies in rpc.php Still need to clean up the creation, but this moves it out of the Rpc class commit d2adc24cb999d0a1dacba3c7134b52fe522d190c Author: Michael J. Rubinsky Date: Mon Feb 1 09:31:50 2010 -0500 Try to figure out how to pass the logger around more cleanly commit d6963e4ab9d773ba484a60f548cbdc3e01a167f8 Author: Michael J. Rubinsky Date: Sun Jan 31 15:36:55 2010 -0500 fix debug method commit 7a65db9b4c8a01d658f1f766ef83a69a000816bf Author: Michael J. Rubinsky Date: Sun Jan 31 15:01:05 2010 -0500 allow multienums to be optionally populated from *_Application::initPrefs() the same as select prefs commit dc21a9daf7cd2f35171c0a31de8df2bc850124f4 Author: Michael J. Rubinsky Date: Sun Jan 31 14:56:43 2010 -0500 wrapping, update phpdoc commit d3da7f7c0f9f6962063687d3c004dfa2bd0daba7 Author: Michael J. Rubinsky Date: Sun Jan 31 10:29:18 2010 -0500 Add missing parameter to contacts/replace Two way contact syncing is now working :) commit 7783613e5d82a0c4b1b9b4d1d5cb598ea553a297 Author: Michael J. Rubinsky Date: Sun Jan 31 10:28:51 2010 -0500 typo, remove TODO commit e0a84ad852f3758bbe6e5d0bd8d61a5253cbffea Author: Michael J. Rubinsky Date: Sun Jan 31 10:27:09 2010 -0500 Viral typo commit 77d123025493384c6359ff0ee0fe5b12f2d816a5 Author: Michael J. Rubinsky Date: Sun Jan 31 10:26:40 2010 -0500 ws, cs commit d86a94c6cd3cd4e17f057ad891f8f0709ab1408c Author: Michael J. Rubinsky Date: Sat Jan 30 17:30:53 2010 -0500 Better way of detecting what object type we are dealing with... One way syncing of contacts from server -> PIM working now commit 629425f5e0d0fbfbb3400f7ac4a2aec7a7b83bff Author: Michael J. Rubinsky Date: Sat Jan 30 17:13:07 2010 -0500 Logging, typos, ws, and a quick fix that will be refactored in the next commit commit 259474126ac77a607bd814e493f7ebfd1c11d99f Author: Michael J. Rubinsky Date: Sat Jan 30 10:48:35 2010 -0500 Some TODO, small fixes commit 302036fc40f01b241fc4b654935a95dc53f3b7f3 Author: Michael J. Rubinsky Date: Sat Jan 30 10:43:35 2010 -0500 A bunch of small fixes, cleanups - PIM still not accepting sent messages commit 9d33cf0e04598396938a50375f0d2f637e8e74ed Author: Michael J. Rubinsky Date: Wed Jan 27 20:31:02 2010 -0500 These need to call getElement(), not _getElement() so _unGetElement() works as expected commit 23f541ce964ff4140e3c8cfe1c7696d129542e8e Author: Michael J. Rubinsky Date: Wed Jan 27 20:02:15 2010 -0500 No longer need these methods commit abdecae7078caa956980685c599f73d25de2c13d Author: Michael J. Rubinsky Date: Wed Jan 27 19:49:31 2010 -0500 Parse errors, fix member names, logging etc...and we have a sort-of-working SYNC_FOLDERS command :) commit c7394d52d020b878497648cb30c16d0a3eda29d4 Author: Michael J. Rubinsky Date: Wed Jan 27 12:53:30 2010 -0500 Add test for GetFolderList command commit 756fb644d57845e6d24a46463fac2acf233a77fc Author: Michael J. Rubinsky Date: Wed Jan 27 11:33:12 2010 -0500 Move these header groupings into ActiveSync Horde_Rpc shouldn't need to know anything anout the AS protocol commit 6d402a37aa00477ff05b05ea8282e76fa1964f7b Author: Michael J. Rubinsky Date: Wed Jan 27 11:31:15 2010 -0500 Need to check for activesync server first, since activesync clients also send and OPTIONS request commit fd7495f995e09148fe66f754f9e441e41b6d022d Author: Michael J. Rubinsky Date: Mon Jan 25 18:26:38 2010 -0500 Short circuit method call if no User parameter set Also, no reason to keep this response commented out...at least right now. commit 89e5543ce42475f76db41bc75b98a13cf10fe347 Author: Michael J. Rubinsky Date: Mon Jan 25 18:25:19 2010 -0500 Fix constant names, default value for $_stack commit 1aa52399d2909d022aaf5cedc7f4b3838c52acde Author: Michael J. Rubinsky Date: Mon Jan 25 18:24:37 2010 -0500 add missing getState() call, fix phpdoc commit 2d340e8cece26ef3ef17ba3d9c4ea09c680d6f19 Author: Michael J. Rubinsky Date: Mon Jan 25 16:47:43 2010 -0500 need to inject $registry into the horde connector commit d81c7bf603eeb44d1c69473a7616daef7d0258e4 Author: Michael J. Rubinsky Date: Mon Jan 25 16:47:16 2010 -0500 Don't attempt to call setLogger() if we don't have one commit 6c32285ed30fd772b38a17a1a04d0349f9a80956 Author: Michael J. Rubinsky Date: Mon Jan 25 16:46:08 2010 -0500 Fix method names, use a setter for logger commit f7ec4c79e3b55484f3c81c2ab4edcf664d97a27a Author: Michael J. Rubinsky Date: Mon Jan 25 16:45:14 2010 -0500 Match the function signature commit e8bcd27502a3b876e94e085527474e538d7fe8d5 Author: Michael J. Rubinsky Date: Mon Jan 25 16:44:49 2010 -0500 Need to mark this class as abstract commit 24a554baf19cc9d3c1d881e491b453ca524ff6eb Author: Michael J. Rubinsky Date: Mon Jan 25 16:44:28 2010 -0500 typo commit ccab379580af3daa9b2f86dc0d200c8b5b50585b Author: Michael J. Rubinsky Date: Mon Jan 25 16:43:51 2010 -0500 Fix type hints commit 4894065458c018a9bb3385b37b8f72cd24be2705 Author: Michael J. Rubinsky Date: Mon Jan 25 15:48:22 2010 -0500 move appInit() to top of file Need to instantiate registry before we instantiate a request object or we end up with warnings about session already started commit ca2257debd3a2ed7837bf9f858d9803173e0d6e6 Author: Michael J. Rubinsky Date: Mon Jan 25 14:57:00 2010 -0500 fix improper rebase/merge conflict resolution commit 3d7d018e18fd19f21e1db69bd2d71570b956cbac Author: Michael J. Rubinsky Date: Mon Jan 25 14:41:04 2010 -0500 I can't seem to type 'connector' without typing 'connectory' for some reason commit bf9055692aa70240c02c63ca642e99cfffcff444 Author: Michael J. Rubinsky Date: Thu Jan 14 12:36:53 2010 -0500 Inject a Horde_Controller_Request object into Horde_Rpc. Use Horde_Controller_Request to obtain all server, post, get, etc.. values instead of accessing super globals directly. So far, only ActiveSync server makes any use of it, the rest of the classes still need to be refactored. commit dbdad6baae8ec769fee65d843373d964d755b4b1 Author: Michael J. Rubinsky Date: Fri Jan 22 18:58:39 2010 -0500 Remove extra slash now that this url comes from Horde::getServiceLink Get rid of these locally defined variables that don't seem to be used commit dc7e70d592bc2c8d5d1f9ff913dd84148e1953d4 Author: Michael J. Rubinsky Date: Wed Jan 20 18:39:14 2010 -0500 This isn't used anywhere commit 8755924ab1181e91bb9633bb40b535a91912b2da Author: Michael J. Rubinsky Date: Mon Jan 25 11:46:40 2010 -0500 Track changes to ActiveSync Yes, we will need to inject this stuff, but Horde_Rpc doesn't support it yet. commit c6bb73124248e746dbbd16c170c72bd4b5fcb720 Author: Michael J. Rubinsky Date: Mon Jan 25 11:08:02 2010 -0500 update package.xml commit 4eb39d86bdac9149979998c3c8e7e1a14e8f4ab0 Author: Michael J. Rubinsky Date: Sun Jan 24 21:36:22 2010 -0500 Add a changeMessage test Sort of works, but can't get the expected text to match the actual text of the vCard - think it's in line endings somewhere. commit 3305f3df71e1c9101f13730a525271b455aec9a7 Author: Michael J. Rubinsky Date: Sun Jan 24 21:35:51 2010 -0500 typos, etc... commit c421d22e5d5bd0c86dc36c868c868e75bbadf7be Author: Michael J. Rubinsky Date: Sun Jan 24 17:57:56 2010 -0500 Add testGetMessage() commit 8dbf25fc37b2bbcd5b950b866cb7f6efab1a425e Author: Michael J. Rubinsky Date: Sun Jan 24 17:57:00 2010 -0500 Some more small fixes found with unit tests commit 0d3fe8eb9a79a4e6efc04227d638c8de7630e574 Author: Michael J. Rubinsky Date: Sun Jan 24 17:54:52 2010 -0500 this can be called statically, this avoids warnings in unit tests commit a7c2a6ec4689e3cc65c71f0888d34bb9144d656d Author: Michael J. Rubinsky Date: Sun Jan 24 17:20:01 2010 -0500 Initial, very basic unit tests commit 7496abbf9e109e66434c5c633f6e16481704b537 Author: Michael J. Rubinsky Date: Sun Jan 24 17:18:42 2010 -0500 Fix method visibility in new connector object, misc. other fixes discovered from first unit tests commit 7a938e72435e23324cf5e8d6ddebbb6687aa3827 Author: Michael J. Rubinsky Date: Sun Jan 24 16:09:10 2010 -0500 Abstract out the connector code. Separate out the code that calls the registry so it can be injected, easier testing, extending etc... commit 0f9102b082a46c7d00900930af0d3b8913002592 Author: Michael J. Rubinsky Date: Thu Jan 21 17:27:39 2010 -0500 Remove the rest of the iCalendar parsing code, implement _toVCard() _fromVCard() Still need to implement the vCalendar parsing and fill in a lot of misisng fields from the vCard, but need to get more familiar with what is actually sent from ActiveSync devices first. commit 743fa07c0e3b858e4bc7d5bf4ce16a7d0d02d9de Author: Michael J. Rubinsky Date: Wed Jan 20 20:37:00 2010 -0500 Start replacing vCard/vCalendar functionality with Horde libraries also phpdoc, random cleanup commit 53f63c63b9e5d97713a9c62e3326296c1771021c Author: Michael J. Rubinsky Date: Tue Jan 19 09:57:37 2010 -0500 package.xml commit a0b7270a9275f6527a95dfb8bd2cdff54bcad0f2 Author: Michael J. Rubinsky Date: Tue Jan 19 09:31:11 2010 -0500 Shorten these class names also commit d8a3185eead0498eac888363e1ec211852b418d8 Author: Michael J. Rubinsky Date: Tue Jan 19 09:13:45 2010 -0500 Shorten class names a bit, the 'Sync' didn't add anything to the name's meaning commit 16f4d870440e9411fb642f5a35df4cc94ad8da7e Author: Michael J. Rubinsky Date: Tue Jan 19 09:09:03 2010 -0500 Rename base class to *_Base commit 4a63e9b9af9eb9bd0e3c9b6f3b65bb78a8fc1a0c Author: Michael J. Rubinsky Date: Thu Jan 14 12:36:53 2010 -0500 Inject a Horde_Controller_Request object into Horde_Rpc. Use Horde_Controller_Request to obtain all server, post, get, etc.. values instead of accessing super globals directly. So far, only ActiveSync server makes any use of it, the rest of the classes still need to be refactored. commit 00ea5c6e9bd905ae5f1f15e6ba7af4b66f5e4fe8 Author: Michael J. Rubinsky Date: Sun Jan 17 12:47:48 2010 -0500 fix sql creation script name commit fd13ca13997f26e370059f4ce91b1c245ee8b018 Author: Michael J. Rubinsky Date: Mon Jan 18 23:23:55 2010 -0500 Add abstract methods to base class, phpdoc commit 33008c4d1fb5a926c561864a24f2c428dc7dc15e Author: Michael J. Rubinsky Date: Mon Jan 18 23:09:01 2010 -0500 Some comments to help me remember what does what.. commit 02f21e5965099348e6298711f4f5eaac742b41e6 Author: Michael J. Rubinsky Date: Mon Jan 18 22:40:16 2010 -0500 ...and another missing method commit 8f0bcee934dc63388e61b0274583ddba6cca0714 Author: Michael J. Rubinsky Date: Mon Jan 18 22:38:06 2010 -0500 Yet Another method that is missing from Z-Push :/ commit 50401cbf1bcfd428d3a78f5fd12b3d4cda2327d0 Author: Michael J. Rubinsky Date: Mon Jan 18 22:06:02 2010 -0500 Tweak const'r parameters, standardize, use setter for logger commit 8868c13528086521e173c601c10a595746f426d3 Author: Michael J. Rubinsky Date: Mon Jan 18 19:39:12 2010 -0500 Reorder parameters so $domain is optional commit 54c61338751147794f3a9f05150353c0c2df4d89 Author: Michael J. Rubinsky Date: Mon Jan 18 19:32:20 2010 -0500 No longer need to pass anything into a const'r for this class commit c9d69bea7467a395e016b95f9333d1fde162c83e Author: Michael J. Rubinsky Date: Mon Jan 18 19:29:35 2010 -0500 Don't store the device id in the backend driver, pass it in any methods that really need it commit 8479524a98c532ee4a8098ca4626f3380341d3e9 Author: Michael J. Rubinsky Date: Mon Jan 18 19:14:41 2010 -0500 Remove protocolversion from the driver classes, doesn't belong there. ...and, in fact, was never used there. commit e88d67839b2dd692995bb5782f7c7dfd4c40bb7f Author: Michael J. Rubinsky Date: Mon Jan 18 19:10:41 2010 -0500 Call the parent's method also commit 34864a9afd29eef6f1d521e8c62ae5adebfae119 Author: Michael J. Rubinsky Date: Mon Jan 18 19:10:18 2010 -0500 phpdoc commit e48b83c4d95a85e2cd810ff1076abf62fc75448d Author: Michael J. Rubinsky Date: Mon Jan 18 19:08:56 2010 -0500 Rename H_ActiveSync_DiffState to H_ActiveSync_DiffState_Base Fill in some phpdoc, clarify some things as I go commit 87c1d638eb0f9e8fd0691a39932c591fe04a08a0 Author: Michael J. Rubinsky Date: Mon Jan 18 19:06:22 2010 -0500 start filling in debug statements commit 8e78650100c5b7c25422963e1841a08958ee0a40 Author: Michael J. Rubinsky Date: Sun Jan 17 16:29:18 2010 -0500 Clean up const'r, use setters for the logger, track changes etc.. Horde_ActiveSync is getting more DI-friendly, but now some of the objects are being instantiated in Horde_Rpc - need to fix this as Rpc gets refactored... commit e4c0b855dfc06b8781e3e6ee5ffb62ca61bcb141 Author: Michael J. Rubinsky Date: Sun Jan 17 16:21:15 2010 -0500 Use the _driver member instead of passing this around everywhere commit d2d05f7f87e7e00fbbd5df85b12df7fe75d90cfe Author: Michael J. Rubinsky Date: Sun Jan 17 16:20:59 2010 -0500 use a setter instead of cont'r parameters commit c08b6a24f571f59c072c06274d8421a28f8163de Author: Michael J. Rubinsky Date: Sun Jan 17 15:46:22 2010 -0500 Use the StateMachine for saving / getting device ping state commit c1eb6630f337c52e8ea169471f226b6be59a8bd7 Author: Michael J. Rubinsky Date: Sun Jan 17 15:25:43 2010 -0500 Horde_ActiveSync:: is going to need a H_Controller_Request object also commit 11a5845843d3c9b273f0ce2e63d02f9f2155911d Author: Michael J. Rubinsky Date: Sun Jan 17 14:59:05 2010 -0500 ...and actually remove it commit 4cb9f8a4f8ad4373afa89a1e88ab3b307909e918 Author: Michael J. Rubinsky Date: Sun Jan 17 14:57:55 2010 -0500 Move this method to the only class that uses it commit c8745a172171538eb19ca1b30f955a805a83d0f0 Author: Michael J. Rubinsky Date: Sun Jan 17 14:36:25 2010 -0500 remove obsolete code, fix global replace error that would have been a real pain to find later... commit 9363ed3e6629de69017dcb2a7586dd83342b35f7 Author: Michael J. Rubinsky Date: Sun Jan 17 14:32:18 2010 -0500 Remove @todo, the streams are only used in the encoder/decoder so we don't need them here at all commit 51012e432f3feff33088b53a2b21a70f27b156bf Author: Michael J. Rubinsky Date: Sun Jan 17 14:29:01 2010 -0500 Inject the encoder and decoder commit d5f640f72b5d8aa08eb9e307c7bdbc855cb0953b Author: Michael J. Rubinsky Date: Sun Jan 17 11:22:19 2010 -0500 Remove obsolete methods commit 72fb56cb9c16b85a6c8b4594f1559f2963138c7c Author: Michael J. Rubinsky Date: Sun Jan 17 11:19:12 2010 -0500 Add methods to StateMachine to save device folder state commit 3bd070495bd17746300ba5785feda306d082bfff Author: Michael J. Rubinsky Date: Sun Jan 17 10:24:44 2010 -0500 remove StateStorage files, track class name changes commit de70dfae406fc498e5035692b6274a74b5b16d6a Author: Michael J. Rubinsky Date: Sun Jan 17 10:22:09 2010 -0500 Don't require the syncKey to be known during instantiation. Rename StateStorage -> StateMachine Don't require the syncKey when object is instantiated, so we can inject the class before we know it. commit ae1ab90fd4366d16bd81532facc7285ddb28378f Author: Michael J. Rubinsky Date: Sat Jan 16 18:57:02 2010 -0500 Add basic filebased state machine commit fa012a9062be95a417674c2e79e2d45b0395ad09 Author: Michael J. Rubinsky Date: Sat Jan 16 18:22:55 2010 -0500 State storage that is injectable The rest of the code still needs to be refactored to use it... commit 6be49bbb1aa34ae4de8aa2a5c88582992dc76a74 Author: Michael J. Rubinsky Date: Sat Jan 16 18:19:42 2010 -0500 Clean up the const'r, add initial stub class for a state storage class commit 3fc220ebd652118d77fef65e5ebb90c2057b72e4 Author: Michael J. Rubinsky Date: Sat Jan 16 17:48:17 2010 -0500 Use type hints in the const'r, and use a setter for the looger commit 741374a227b2adb5756de3ff9fe156a1b5b0ac5c Author: Michael J. Rubinsky Date: Sat Jan 16 16:34:37 2010 -0500 Send output back to PDA commit 2c41718a969a666384e54a1e0926351482e0f7ff Author: Michael J. Rubinsky Date: Sat Jan 16 16:34:05 2010 -0500 ws, phpdoc commit 11f132291b5a3e910cd251ea5d9c7232d4c6c979 Author: Michael J. Rubinsky Date: Sat Jan 16 16:14:02 2010 -0500 Have Horde_ActiveSync be responsible for sending ActiveSync specific headers commit 28209901a1d5fa48bc9577ce34d31e454e9dce20 Author: Michael J. Rubinsky Date: Sat Jan 16 14:08:05 2010 -0500 remove conflict artifact commit 3d9a54884da6b01942d52ad25686bbe8fcee5ecc Author: Michael J. Rubinsky Date: Thu Jan 14 12:36:53 2010 -0500 Inject a Horde_Controller_Request object into Horde_Rpc. Use Horde_Controller_Request to obtain all server, post, get, etc.. values instead of accessing super globals directly. So far, only ActiveSync server makes any use of it, the rest of the classes still need to be refactored. commit f33828a01255553bed795bdac1eb1d5f79a888b6 Author: Michael J. Rubinsky Date: Thu Jan 14 22:28:34 2010 -0500 Add basic response handling from Rpc. Most of the header ouput currently done here should be moved to the Horde_ActiveSync objects once I figure out the best place for them there. commit 59275a8a2dde5097c25c809ad2c6a6793e7c4970 Author: Michael J. Rubinsky Date: Thu Jan 14 22:12:53 2010 -0500 No need for a separate member for this, just use it more intelligently commit 07e061dfb654f195053d5f63baefd8865b2462e5 Author: Michael J. Rubinsky Date: Thu Jan 14 22:05:11 2010 -0500 Check for policy header / provisioning requests, phpdoc commit 538b8f37c5b229fc0b894960953e71c3ff1a93b7 Author: Michael J. Rubinsky Date: Thu Jan 14 22:04:31 2010 -0500 phpdoc, pass in a horde_log_logger commit 2a7b50fe4bf1b2ab43308b25c176f79d07559219 Author: Michael J. Rubinsky Date: Thu Jan 14 14:44:09 2010 -0500 Authenticate against Horde in the Horde driver. Plus some phpdoc, parameter tweaking to better support injection etc... still lots to do.. commit 599ec3bb82c464a0b09e79d5a92580e276eeb361 Author: Michael J. Rubinsky Date: Thu Jan 14 14:33:25 2010 -0500 Override Horde_Rpc#authenticate so ActiveSync can handle it Allows activesync to authenticate to any backend, not just a horde backend. commit a88fbd976e9616b0f21f5d0f945a81a4fa821298 Author: Michael J. Rubinsky Date: Thu Jan 14 12:56:40 2010 -0500 Actually, should do this in the const'r commit 16b9b3f626b5d671275fb63c96a9a97f3cefcde9 Author: Michael J. Rubinsky Date: Thu Jan 14 12:54:46 2010 -0500 Start filling in some logic. Check for required GET parameters for ActiveSync Requests commit fb4aad137d439c609bbc3c5e0fbc7171210f35f7 Author: Michael J. Rubinsky Date: Thu Jan 14 12:41:01 2010 -0500 phpdoc commit 604c64485e12ad589e2af0bb7c1df27224b2308c Author: Michael J. Rubinsky Date: Thu Jan 14 12:36:53 2010 -0500 Inject a Horde_Controller_Request object into Horde_Rpc. Use Horde_Controller_Request to obtain all server, post, get, etc.. values instead of accessing super globals directly. So far, only ActiveSync server makes any use of it, the rest of the classes still need to be refactored. commit 63308b3e8366bdfe5bd7ec7d1f494ae57dc43423 Author: Michael J. Rubinsky Date: Fri Jan 8 13:28:22 2010 -0500 Allow RPC backends to handle their own output back to the client. Needed since ActiveSync streams results directly back to client while the results are being built in order to reduce the memory footprint. commit 6d0d138e4c84c862b865b205b96bb267b0f7db55 Author: Michael J. Rubinsky Date: Tue Jan 5 13:04:46 2010 -0500 Add stubs for ActiveSync commit 58ee7245d8b39ef70de481781d53d32c52f11ef1 Author: Michael J. Rubinsky Date: Thu Dec 31 13:52:03 2009 -0500 Add support for detecting ActiveSync requests commit 175bcc4d6ce2aef28119ed4a939fd8a545048c45 Author: Michael J. Rubinsky Date: Tue Jan 5 10:42:21 2010 -0500 More cleanup for wbxml code, CS, logging, PHP5-ify etc... commit ab2cfde4b1125a10be0f930e183047f7012e202e Author: Michael J. Rubinsky Date: Mon Jan 4 17:54:12 2010 -0500 Refactor Z-Push's wbxml handling into Horde_ActiveSync. Eventually this should be refactored to use Horde's WBXML library, but it's a bit hard to grock for me at the moment. commit 55e1fa23cec6c03a6552ce2362fb101f902e17f1 Author: Michael J. Rubinsky Date: Mon Jan 4 15:42:22 2010 -0500 More CS, Hordifications, etc... commit f903e5c39e13d06cd1ec7e421e03c07e7a1831ac Author: Michael J. Rubinsky Date: Mon Jan 4 15:08:09 2010 -0500 CS, const'r, logging, general Hordify-ing commit 4df017f7a9b792f6d7f001a96f09b798bcd4bbb5 Author: Michael J. Rubinsky Date: Mon Jan 4 11:57:53 2010 -0500 These don't seem to be used anywhere commit e712da7baeb4926bcf8c8cab468fb1610a829ce8 Author: Michael J. Rubinsky Date: Mon Jan 4 11:55:03 2010 -0500 Pass logger to DiffState objects and lots of CS cleanup as I see it. commit f3e6c3ea7dec04a9b11a90c588b8de8a35e98f1e Author: Michael J. Rubinsky Date: Mon Jan 4 10:13:31 2010 -0500 Pass the logger object to the streamers commit afd5ceb944f6f23866075c0656832fe86126170e Author: Michael J. Rubinsky Date: Sun Jan 3 16:46:39 2010 -0500 fix function signature, CS commit 98139593495a8d66dc62c0c5ba5feacce19ea688 Author: Michael J. Rubinsky Date: Sun Jan 3 16:45:43 2010 -0500 Use a Horde_Log_Logger for debug and error output commit f2fd2e7704909777b428fc557706fb056a80800e Author: Michael J. Rubinsky Date: Sun Jan 3 13:00:55 2010 -0500 pass policykey/policies in const'r commit 6c363b8da893a1e2d9948493849563f965793dc9 Author: Michael J. Rubinsky Date: Sun Jan 3 12:52:32 2010 -0500 Pass username/password in from const'r commit 955b314f56fea3c67695fdfe806317e6e84a6ad3 Author: Michael J. Rubinsky Date: Sun Jan 3 12:33:10 2010 -0500 pass stateDir and logger in const'r commit 2df456c43b681c96b62286af89a92ec969dc4199 Author: Michael J. Rubinsky Date: Sun Jan 3 12:12:42 2010 -0500 make input/output streams protected class members commit e1e3f5631f34989739c409e727f804c4682cea50 Author: Michael J. Rubinsky Date: Sun Jan 3 11:53:08 2010 -0500 First round of massive refactoring of Z-Push --- ansel/lib/Storage.php | 1 - framework/ActiveSync/lib/Horde/ActiveSync.php | 1875 ++++++++++++++++++++ .../lib/Horde/ActiveSync/ActiveSync/Factory.php | 33 + .../lib/Horde/ActiveSync/ContentsCache.php | 16 + .../lib/Horde/ActiveSync/Driver/Base.php | 578 ++++++ .../lib/Horde/ActiveSync/Driver/Horde.php | 725 ++++++++ .../ActiveSync/Driver/Horde/Connector/Registry.php | 217 +++ .../ActiveSync/lib/Horde/ActiveSync/Exception.php | 3 + .../ActiveSync/lib/Horde/ActiveSync/Exporter.php | 268 +++ .../lib/Horde/ActiveSync/HierarchyCache.php | 47 + .../ActiveSync/lib/Horde/ActiveSync/Importer.php | 261 +++ .../lib/Horde/ActiveSync/Message/Appointment.php | 722 ++++++++ .../lib/Horde/ActiveSync/Message/Attendee.php | 27 + .../lib/Horde/ActiveSync/Message/Base.php | 384 ++++ .../lib/Horde/ActiveSync/Message/Contact.php | 151 ++ .../lib/Horde/ActiveSync/Message/Exception.php | 52 + .../lib/Horde/ActiveSync/Message/Folder.php | 34 + .../lib/Horde/ActiveSync/Message/Recurrence.php | 48 + .../lib/Horde/ActiveSync/Request/Base.php | 130 ++ .../lib/Horde/ActiveSync/Request/FolderSync.php | 214 +++ .../lib/Horde/ActiveSync/Request/Options.php | 26 + .../lib/Horde/ActiveSync/Request/Ping.php | 183 ++ .../lib/Horde/ActiveSync/Request/Provision.php | 173 ++ .../lib/Horde/ActiveSync/Request/Sync.php | 529 ++++++ .../ActiveSync/lib/Horde/ActiveSync/State/Base.php | 291 +++ .../ActiveSync/lib/Horde/ActiveSync/State/File.php | 623 +++++++ .../lib/Horde/ActiveSync/State/History.php | 595 +++++++ .../ActiveSync/lib/Horde/ActiveSync/Streamer.php | 124 ++ .../ActiveSync/lib/Horde/ActiveSync/Timezone.php | 199 +++ .../ActiveSync/lib/Horde/ActiveSync/Wbxml.php | 56 + .../lib/Horde/ActiveSync/Wbxml/Decoder.php | 590 ++++++ .../lib/Horde/ActiveSync/Wbxml/Encoder.php | 352 ++++ framework/ActiveSync/package.xml | 131 ++ .../ActiveSync/test/Horde/ActiveSync/AllTests.php | 35 + .../test/Horde/ActiveSync/FileStateTest.php | 136 ++ .../test/Horde/ActiveSync/HordeDriverTest.php | 270 +++ .../test/Horde/ActiveSync/TimezoneTest.php | 123 ++ .../Horde/ActiveSync/fixtures/MockConnector.php | 76 + framework/Date/lib/Horde/Date.php | 1 + framework/Rpc/lib/Horde/Rpc.php | 65 +- framework/Rpc/lib/Horde/Rpc/ActiveSync.php | 264 +++ framework/Rpc/lib/Horde/Rpc/Jsonrpc.php | 4 +- framework/Rpc/lib/Horde/Rpc/Phpgw.php | 4 +- framework/Rpc/lib/Horde/Rpc/Soap.php | 4 +- framework/Rpc/lib/Horde/Rpc/Webdav.php | 8 +- framework/Rpc/lib/Horde/Rpc/Xmlrpc.php | 4 +- framework/Rpc/package.xml | 2 + framework/iCalendar/iCalendar/vcard.php | 2 +- horde/config/conf.xml | 13 + horde/rpc.php | 66 +- kronolith/edit.php | 2 + kronolith/lib/Api.php | 31 +- kronolith/lib/Driver/Sql.php | 33 +- kronolith/lib/Event.php | 178 ++ kronolith/lib/Event/Sql.php | 8 + kronolith/lib/Kronolith.php | 10 +- kronolith/scripts/sql/kronolith.mssql.sql | 1 + kronolith/scripts/sql/kronolith.mysql.sql | 1 + kronolith/scripts/sql/kronolith.oci8.sql | 2 + kronolith/scripts/sql/kronolith.pgsql.sql | 1 + kronolith/scripts/sql/kronolith.sql | 1 + kronolith/scripts/sql/kronolith.xml | 6 + .../scripts/upgrades/2010-03-25_add_baseid.sql | 1 + turba/lib/Api.php | 14 +- 64 files changed, 10966 insertions(+), 58 deletions(-) create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/ActiveSync/Factory.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/ContentsCache.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Driver/Base.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde/Connector/Registry.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Exception.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Exporter.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/HierarchyCache.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Importer.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Message/Appointment.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Message/Attendee.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Message/Contact.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Message/Exception.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Message/Folder.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Message/Recurrence.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Request/Base.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Request/Options.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Request/Ping.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Request/Provision.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/State/Base.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/State/File.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/State/History.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Streamer.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Timezone.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Wbxml.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Decoder.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Encoder.php create mode 100644 framework/ActiveSync/package.xml create mode 100644 framework/ActiveSync/test/Horde/ActiveSync/AllTests.php create mode 100644 framework/ActiveSync/test/Horde/ActiveSync/FileStateTest.php create mode 100644 framework/ActiveSync/test/Horde/ActiveSync/HordeDriverTest.php create mode 100644 framework/ActiveSync/test/Horde/ActiveSync/TimezoneTest.php create mode 100644 framework/ActiveSync/test/Horde/ActiveSync/fixtures/MockConnector.php create mode 100644 framework/Rpc/lib/Horde/Rpc/ActiveSync.php create mode 100644 kronolith/scripts/upgrades/2010-03-25_add_baseid.sql diff --git a/ansel/lib/Storage.php b/ansel/lib/Storage.php index a97dc6a95..522a124d8 100644 --- a/ansel/lib/Storage.php +++ b/ansel/lib/Storage.php @@ -1163,7 +1163,6 @@ class Ansel_Storage $sql = 'SELECT DISTINCT image_location, image_latitude, image_longitude FROM ansel_images WHERE LENGTH(image_location) > 0'; if (strlen($search)) { $sql .= ' AND image_location LIKE ' . $GLOBALS['ansel_db']->quote("$search%"); - } Horde::logMessage(sprintf("SQL QUERY BY Ansel_Storage::searchLocations: %s", $sql), 'DEBUG'); $results = $this->_db->query($sql); diff --git a/framework/ActiveSync/lib/Horde/ActiveSync.php b/framework/ActiveSync/lib/Horde/ActiveSync.php new file mode 100644 index 000000000..88f4d54ce --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync.php @@ -0,0 +1,1875 @@ + + * @package Horde_ActiveSync + */ +/** + * File : diffbackend.php + * Project : Z-Push + * Descr : We do a standard differential + * change detection by sorting both + * lists of items by their unique id, + * and then traversing both arrays + * of items at once. Changes can be + * detected by comparing items at + * the same position in both arrays. + * + * Created : 01.10.2007 + * + * Zarafa Deutschland GmbH, www.zarafaserver.de + * This file is distributed under GPL v2. + * Consult LICENSE file for details + */ +// TODO Class constant these: +define("SYNC_SYNCHRONIZE","Synchronize"); +define("SYNC_REPLIES","Replies"); +define("SYNC_ADD","Add"); +define("SYNC_MODIFY","Modify"); +define("SYNC_REMOVE","Remove"); +define("SYNC_FETCH","Fetch"); +define("SYNC_SYNCKEY","SyncKey"); +define("SYNC_CLIENTENTRYID","ClientEntryId"); +define("SYNC_SERVERENTRYID","ServerEntryId"); +define("SYNC_STATUS","Status"); +define("SYNC_FOLDER","Folder"); +define("SYNC_FOLDERTYPE","FolderType"); +define("SYNC_VERSION","Version"); +define("SYNC_FOLDERID","FolderId"); +define("SYNC_GETCHANGES","GetChanges"); +define("SYNC_MOREAVAILABLE","MoreAvailable"); +define("SYNC_WINDOWSIZE","WindowSize"); +define("SYNC_COMMANDS","Commands"); +define("SYNC_OPTIONS","Options"); +define("SYNC_FILTERTYPE","FilterType"); +define("SYNC_TRUNCATION","Truncation"); +define("SYNC_RTFTRUNCATION","RtfTruncation"); +define("SYNC_CONFLICT","Conflict"); +define("SYNC_FOLDERS","Folders"); +define("SYNC_DATA","Data"); +define("SYNC_DELETESASMOVES","DeletesAsMoves"); +define("SYNC_NOTIFYGUID","NotifyGUID"); +define("SYNC_SUPPORTED","Supported"); +define("SYNC_SOFTDELETE","SoftDelete"); +define("SYNC_MIMESUPPORT","MIMESupport"); +define("SYNC_MIMETRUNCATION","MIMETruncation"); +define("SYNC_NEWMESSAGE","NewMessage"); + +// POOMCONTACTS +define("SYNC_POOMCONTACTS_ANNIVERSARY","POOMCONTACTS:Anniversary"); +define("SYNC_POOMCONTACTS_ASSISTANTNAME","POOMCONTACTS:AssistantName"); +define("SYNC_POOMCONTACTS_ASSISTNAMEPHONENUMBER","POOMCONTACTS:AssistnamePhoneNumber"); +define("SYNC_POOMCONTACTS_BIRTHDAY","POOMCONTACTS:Birthday"); +define("SYNC_POOMCONTACTS_BODY","POOMCONTACTS:Body"); +define("SYNC_POOMCONTACTS_BODYSIZE","POOMCONTACTS:BodySize"); +define("SYNC_POOMCONTACTS_BODYTRUNCATED","POOMCONTACTS:BodyTruncated"); +define("SYNC_POOMCONTACTS_BUSINESS2PHONENUMBER","POOMCONTACTS:Business2PhoneNumber"); +define("SYNC_POOMCONTACTS_BUSINESSCITY","POOMCONTACTS:BusinessCity"); +define("SYNC_POOMCONTACTS_BUSINESSCOUNTRY","POOMCONTACTS:BusinessCountry"); +define("SYNC_POOMCONTACTS_BUSINESSPOSTALCODE","POOMCONTACTS:BusinessPostalCode"); +define("SYNC_POOMCONTACTS_BUSINESSSTATE","POOMCONTACTS:BusinessState"); +define("SYNC_POOMCONTACTS_BUSINESSSTREET","POOMCONTACTS:BusinessStreet"); +define("SYNC_POOMCONTACTS_BUSINESSFAXNUMBER","POOMCONTACTS:BusinessFaxNumber"); +define("SYNC_POOMCONTACTS_BUSINESSPHONENUMBER","POOMCONTACTS:BusinessPhoneNumber"); +define("SYNC_POOMCONTACTS_CARPHONENUMBER","POOMCONTACTS:CarPhoneNumber"); +define("SYNC_POOMCONTACTS_CATEGORIES","POOMCONTACTS:Categories"); +define("SYNC_POOMCONTACTS_CATEGORY","POOMCONTACTS:Category"); +define("SYNC_POOMCONTACTS_CHILDREN","POOMCONTACTS:Children"); +define("SYNC_POOMCONTACTS_CHILD","POOMCONTACTS:Child"); +define("SYNC_POOMCONTACTS_COMPANYNAME","POOMCONTACTS:CompanyName"); +define("SYNC_POOMCONTACTS_DEPARTMENT","POOMCONTACTS:Department"); +define("SYNC_POOMCONTACTS_EMAIL1ADDRESS","POOMCONTACTS:Email1Address"); +define("SYNC_POOMCONTACTS_EMAIL2ADDRESS","POOMCONTACTS:Email2Address"); +define("SYNC_POOMCONTACTS_EMAIL3ADDRESS","POOMCONTACTS:Email3Address"); +define("SYNC_POOMCONTACTS_FILEAS","POOMCONTACTS:FileAs"); +define("SYNC_POOMCONTACTS_FIRSTNAME","POOMCONTACTS:FirstName"); +define("SYNC_POOMCONTACTS_HOME2PHONENUMBER","POOMCONTACTS:Home2PhoneNumber"); +define("SYNC_POOMCONTACTS_HOMECITY","POOMCONTACTS:HomeCity"); +define("SYNC_POOMCONTACTS_HOMECOUNTRY","POOMCONTACTS:HomeCountry"); +define("SYNC_POOMCONTACTS_HOMEPOSTALCODE","POOMCONTACTS:HomePostalCode"); +define("SYNC_POOMCONTACTS_HOMESTATE","POOMCONTACTS:HomeState"); +define("SYNC_POOMCONTACTS_HOMESTREET","POOMCONTACTS:HomeStreet"); +define("SYNC_POOMCONTACTS_HOMEFAXNUMBER","POOMCONTACTS:HomeFaxNumber"); +define("SYNC_POOMCONTACTS_HOMEPHONENUMBER","POOMCONTACTS:HomePhoneNumber"); +define("SYNC_POOMCONTACTS_JOBTITLE","POOMCONTACTS:JobTitle"); +define("SYNC_POOMCONTACTS_LASTNAME","POOMCONTACTS:LastName"); +define("SYNC_POOMCONTACTS_MIDDLENAME","POOMCONTACTS:MiddleName"); +define("SYNC_POOMCONTACTS_MOBILEPHONENUMBER","POOMCONTACTS:MobilePhoneNumber"); +define("SYNC_POOMCONTACTS_OFFICELOCATION","POOMCONTACTS:OfficeLocation"); +define("SYNC_POOMCONTACTS_OTHERCITY","POOMCONTACTS:OtherCity"); +define("SYNC_POOMCONTACTS_OTHERCOUNTRY","POOMCONTACTS:OtherCountry"); +define("SYNC_POOMCONTACTS_OTHERPOSTALCODE","POOMCONTACTS:OtherPostalCode"); +define("SYNC_POOMCONTACTS_OTHERSTATE","POOMCONTACTS:OtherState"); +define("SYNC_POOMCONTACTS_OTHERSTREET","POOMCONTACTS:OtherStreet"); +define("SYNC_POOMCONTACTS_PAGERNUMBER","POOMCONTACTS:PagerNumber"); +define("SYNC_POOMCONTACTS_RADIOPHONENUMBER","POOMCONTACTS:RadioPhoneNumber"); +define("SYNC_POOMCONTACTS_SPOUSE","POOMCONTACTS:Spouse"); +define("SYNC_POOMCONTACTS_SUFFIX","POOMCONTACTS:Suffix"); +define("SYNC_POOMCONTACTS_TITLE","POOMCONTACTS:Title"); +define("SYNC_POOMCONTACTS_WEBPAGE","POOMCONTACTS:WebPage"); +define("SYNC_POOMCONTACTS_YOMICOMPANYNAME","POOMCONTACTS:YomiCompanyName"); +define("SYNC_POOMCONTACTS_YOMIFIRSTNAME","POOMCONTACTS:YomiFirstName"); +define("SYNC_POOMCONTACTS_YOMILASTNAME","POOMCONTACTS:YomiLastName"); +define("SYNC_POOMCONTACTS_RTF","POOMCONTACTS:Rtf"); +define("SYNC_POOMCONTACTS_PICTURE","POOMCONTACTS:Picture"); + +// POOMMAIL +define("SYNC_POOMMAIL_ATTACHMENT","POOMMAIL:Attachment"); +define("SYNC_POOMMAIL_ATTACHMENTS","POOMMAIL:Attachments"); +define("SYNC_POOMMAIL_ATTNAME","POOMMAIL:AttName"); +define("SYNC_POOMMAIL_ATTSIZE","POOMMAIL:AttSize"); +define("SYNC_POOMMAIL_ATTOID","POOMMAIL:AttOid"); +define("SYNC_POOMMAIL_ATTMETHOD","POOMMAIL:AttMethod"); +define("SYNC_POOMMAIL_ATTREMOVED","POOMMAIL:AttRemoved"); +define("SYNC_POOMMAIL_BODY","POOMMAIL:Body"); +define("SYNC_POOMMAIL_BODYSIZE","POOMMAIL:BodySize"); +define("SYNC_POOMMAIL_BODYTRUNCATED","POOMMAIL:BodyTruncated"); +define("SYNC_POOMMAIL_DATERECEIVED","POOMMAIL:DateReceived"); +define("SYNC_POOMMAIL_DISPLAYNAME","POOMMAIL:DisplayName"); +define("SYNC_POOMMAIL_DISPLAYTO","POOMMAIL:DisplayTo"); +define("SYNC_POOMMAIL_IMPORTANCE","POOMMAIL:Importance"); +define("SYNC_POOMMAIL_MESSAGECLASS","POOMMAIL:MessageClass"); +define("SYNC_POOMMAIL_SUBJECT","POOMMAIL:Subject"); +define("SYNC_POOMMAIL_READ","POOMMAIL:Read"); +define("SYNC_POOMMAIL_TO","POOMMAIL:To"); +define("SYNC_POOMMAIL_CC","POOMMAIL:Cc"); +define("SYNC_POOMMAIL_FROM","POOMMAIL:From"); +define("SYNC_POOMMAIL_REPLY_TO","POOMMAIL:Reply-To"); +define("SYNC_POOMMAIL_ALLDAYEVENT","POOMMAIL:AllDayEvent"); +define("SYNC_POOMMAIL_CATEGORIES","POOMMAIL:Categories"); +define("SYNC_POOMMAIL_CATEGORY","POOMMAIL:Category"); +define("SYNC_POOMMAIL_DTSTAMP","POOMMAIL:DtStamp"); +define("SYNC_POOMMAIL_ENDTIME","POOMMAIL:EndTime"); +define("SYNC_POOMMAIL_INSTANCETYPE","POOMMAIL:InstanceType"); +define("SYNC_POOMMAIL_BUSYSTATUS","POOMMAIL:BusyStatus"); +define("SYNC_POOMMAIL_LOCATION","POOMMAIL:Location"); +define("SYNC_POOMMAIL_MEETINGREQUEST","POOMMAIL:MeetingRequest"); +define("SYNC_POOMMAIL_ORGANIZER","POOMMAIL:Organizer"); +define("SYNC_POOMMAIL_RECURRENCEID","POOMMAIL:RecurrenceId"); +define("SYNC_POOMMAIL_REMINDER","POOMMAIL:Reminder"); +define("SYNC_POOMMAIL_RESPONSEREQUESTED","POOMMAIL:ResponseRequested"); +define("SYNC_POOMMAIL_RECURRENCES","POOMMAIL:Recurrences"); +define("SYNC_POOMMAIL_RECURRENCE","POOMMAIL:Recurrence"); +define("SYNC_POOMMAIL_TYPE","POOMMAIL:Type"); +define("SYNC_POOMMAIL_UNTIL","POOMMAIL:Until"); +define("SYNC_POOMMAIL_OCCURRENCES","POOMMAIL:Occurrences"); +define("SYNC_POOMMAIL_INTERVAL","POOMMAIL:Interval"); +define("SYNC_POOMMAIL_DAYOFWEEK","POOMMAIL:DayOfWeek"); +define("SYNC_POOMMAIL_DAYOFMONTH","POOMMAIL:DayOfMonth"); +define("SYNC_POOMMAIL_WEEKOFMONTH","POOMMAIL:WeekOfMonth"); +define("SYNC_POOMMAIL_MONTHOFYEAR","POOMMAIL:MonthOfYear"); +define("SYNC_POOMMAIL_STARTTIME","POOMMAIL:StartTime"); +define("SYNC_POOMMAIL_SENSITIVITY","POOMMAIL:Sensitivity"); +define("SYNC_POOMMAIL_TIMEZONE","POOMMAIL:TimeZone"); +define("SYNC_POOMMAIL_GLOBALOBJID","POOMMAIL:GlobalObjId"); +define("SYNC_POOMMAIL_THREADTOPIC","POOMMAIL:ThreadTopic"); +define("SYNC_POOMMAIL_MIMEDATA","POOMMAIL:MIMEData"); +define("SYNC_POOMMAIL_MIMETRUNCATED","POOMMAIL:MIMETruncated"); +define("SYNC_POOMMAIL_MIMESIZE","POOMMAIL:MIMESize"); +define("SYNC_POOMMAIL_INTERNETCPID","POOMMAIL:InternetCPID"); + +// AIRNOTIFY +define("SYNC_AIRNOTIFY_NOTIFY","AirNotify:Notify"); +define("SYNC_AIRNOTIFY_NOTIFICATION","AirNotify:Notification"); +define("SYNC_AIRNOTIFY_VERSION","AirNotify:Version"); +define("SYNC_AIRNOTIFY_LIFETIME","AirNotify:Lifetime"); +define("SYNC_AIRNOTIFY_DEVICEINFO","AirNotify:DeviceInfo"); +define("SYNC_AIRNOTIFY_ENABLE","AirNotify:Enable"); +define("SYNC_AIRNOTIFY_FOLDER","AirNotify:Folder"); +define("SYNC_AIRNOTIFY_SERVERENTRYID","AirNotify:ServerEntryId"); +define("SYNC_AIRNOTIFY_DEVICEADDRESS","AirNotify:DeviceAddress"); +define("SYNC_AIRNOTIFY_VALIDCARRIERPROFILES","AirNotify:ValidCarrierProfiles"); +define("SYNC_AIRNOTIFY_CARRIERPROFILE","AirNotify:CarrierProfile"); +define("SYNC_AIRNOTIFY_STATUS","AirNotify:Status"); +define("SYNC_AIRNOTIFY_REPLIES","AirNotify:Replies"); +define("SYNC_AIRNOTIFY_VERSION='1.1'","AirNotify:Version='1.1'"); +define("SYNC_AIRNOTIFY_DEVICES","AirNotify:Devices"); +define("SYNC_AIRNOTIFY_DEVICE","AirNotify:Device"); +define("SYNC_AIRNOTIFY_ID","AirNotify:Id"); +define("SYNC_AIRNOTIFY_EXPIRY","AirNotify:Expiry"); +define("SYNC_AIRNOTIFY_NOTIFYGUID","AirNotify:NotifyGUID"); + +// POOMCAL +define("SYNC_POOMCAL_TIMEZONE","POOMCAL:Timezone"); +define("SYNC_POOMCAL_ALLDAYEVENT","POOMCAL:AllDayEvent"); +define("SYNC_POOMCAL_ATTENDEES","POOMCAL:Attendees"); +define("SYNC_POOMCAL_ATTENDEE","POOMCAL:Attendee"); +define("SYNC_POOMCAL_EMAIL","POOMCAL:Email"); +define("SYNC_POOMCAL_NAME","POOMCAL:Name"); +define("SYNC_POOMCAL_BODY","POOMCAL:Body"); +define("SYNC_POOMCAL_BODYTRUNCATED","POOMCAL:BodyTruncated"); +define("SYNC_POOMCAL_BUSYSTATUS","POOMCAL:BusyStatus"); +define("SYNC_POOMCAL_CATEGORIES","POOMCAL:Categories"); +define("SYNC_POOMCAL_CATEGORY","POOMCAL:Category"); +define("SYNC_POOMCAL_RTF","POOMCAL:Rtf"); +define("SYNC_POOMCAL_DTSTAMP","POOMCAL:DtStamp"); +define("SYNC_POOMCAL_ENDTIME","POOMCAL:EndTime"); +define("SYNC_POOMCAL_EXCEPTION","POOMCAL:Exception"); +define("SYNC_POOMCAL_EXCEPTIONS","POOMCAL:Exceptions"); +define("SYNC_POOMCAL_DELETED","POOMCAL:Deleted"); +define("SYNC_POOMCAL_EXCEPTIONSTARTTIME","POOMCAL:ExceptionStartTime"); +define("SYNC_POOMCAL_LOCATION","POOMCAL:Location"); +define("SYNC_POOMCAL_MEETINGSTATUS","POOMCAL:MeetingStatus"); +define("SYNC_POOMCAL_ORGANIZEREMAIL","POOMCAL:OrganizerEmail"); +define("SYNC_POOMCAL_ORGANIZERNAME","POOMCAL:OrganizerName"); +define("SYNC_POOMCAL_RECURRENCE","POOMCAL:Recurrence"); +define("SYNC_POOMCAL_TYPE","POOMCAL:Type"); +define("SYNC_POOMCAL_UNTIL","POOMCAL:Until"); +define("SYNC_POOMCAL_OCCURRENCES","POOMCAL:Occurrences"); +define("SYNC_POOMCAL_INTERVAL","POOMCAL:Interval"); +define("SYNC_POOMCAL_DAYOFWEEK","POOMCAL:DayOfWeek"); +define("SYNC_POOMCAL_DAYOFMONTH","POOMCAL:DayOfMonth"); +define("SYNC_POOMCAL_WEEKOFMONTH","POOMCAL:WeekOfMonth"); +define("SYNC_POOMCAL_MONTHOFYEAR","POOMCAL:MonthOfYear"); +define("SYNC_POOMCAL_REMINDER","POOMCAL:Reminder"); +define("SYNC_POOMCAL_SENSITIVITY","POOMCAL:Sensitivity"); +define("SYNC_POOMCAL_SUBJECT","POOMCAL:Subject"); +define("SYNC_POOMCAL_STARTTIME","POOMCAL:StartTime"); +define("SYNC_POOMCAL_UID","POOMCAL:UID"); +define("SYNC_POOMCAL_RESPONSETYPE", "POOMCAL:ResponseType"); + +// Move +define("SYNC_MOVE_MOVES","Move:Moves"); +define("SYNC_MOVE_MOVE","Move:Move"); +define("SYNC_MOVE_SRCMSGID","Move:SrcMsgId"); +define("SYNC_MOVE_SRCFLDID","Move:SrcFldId"); +define("SYNC_MOVE_DSTFLDID","Move:DstFldId"); +define("SYNC_MOVE_RESPONSE","Move:Response"); +define("SYNC_MOVE_STATUS","Move:Status"); +define("SYNC_MOVE_DSTMSGID","Move:DstMsgId"); + +// GetItemEstimate +define("SYNC_GETITEMESTIMATE_GETITEMESTIMATE","GetItemEstimate:GetItemEstimate"); +define("SYNC_GETITEMESTIMATE_VERSION","GetItemEstimate:Version"); +define("SYNC_GETITEMESTIMATE_FOLDERS","GetItemEstimate:Folders"); +define("SYNC_GETITEMESTIMATE_FOLDER","GetItemEstimate:Folder"); +define("SYNC_GETITEMESTIMATE_FOLDERTYPE","GetItemEstimate:FolderType"); +define("SYNC_GETITEMESTIMATE_FOLDERID","GetItemEstimate:FolderId"); +define("SYNC_GETITEMESTIMATE_DATETIME","GetItemEstimate:DateTime"); +define("SYNC_GETITEMESTIMATE_ESTIMATE","GetItemEstimate:Estimate"); +define("SYNC_GETITEMESTIMATE_RESPONSE","GetItemEstimate:Response"); +define("SYNC_GETITEMESTIMATE_STATUS","GetItemEstimate:Status"); + +// FolderHierarchy +define("SYNC_FOLDERHIERARCHY_FOLDERS","FolderHierarchy:Folders"); +define("SYNC_FOLDERHIERARCHY_FOLDER","FolderHierarchy:Folder"); +define("SYNC_FOLDERHIERARCHY_DISPLAYNAME","FolderHierarchy:DisplayName"); +define("SYNC_FOLDERHIERARCHY_SERVERENTRYID","FolderHierarchy:ServerEntryId"); +define("SYNC_FOLDERHIERARCHY_PARENTID","FolderHierarchy:ParentId"); +define("SYNC_FOLDERHIERARCHY_TYPE","FolderHierarchy:Type"); +define("SYNC_FOLDERHIERARCHY_RESPONSE","FolderHierarchy:Response"); +define("SYNC_FOLDERHIERARCHY_STATUS","FolderHierarchy:Status"); +define("SYNC_FOLDERHIERARCHY_CONTENTCLASS","FolderHierarchy:ContentClass"); +define("SYNC_FOLDERHIERARCHY_CHANGES","FolderHierarchy:Changes"); +define("SYNC_FOLDERHIERARCHY_ADD","FolderHierarchy:Add"); +define("SYNC_FOLDERHIERARCHY_REMOVE","FolderHierarchy:Remove"); +define("SYNC_FOLDERHIERARCHY_UPDATE","FolderHierarchy:Update"); +define("SYNC_FOLDERHIERARCHY_SYNCKEY","FolderHierarchy:SyncKey"); +define("SYNC_FOLDERHIERARCHY_FOLDERCREATE","FolderHierarchy:FolderCreate"); +define("SYNC_FOLDERHIERARCHY_FOLDERDELETE","FolderHierarchy:FolderDelete"); +define("SYNC_FOLDERHIERARCHY_FOLDERUPDATE","FolderHierarchy:FolderUpdate"); +define("SYNC_FOLDERHIERARCHY_FOLDERSYNC","FolderHierarchy:FolderSync"); +define("SYNC_FOLDERHIERARCHY_COUNT","FolderHierarchy:Count"); +define("SYNC_FOLDERHIERARCHY_VERSION","FolderHierarchy:Version"); + +// MeetingResponse +define("SYNC_MEETINGRESPONSE_CALENDARID","MeetingResponse:CalendarId"); +define("SYNC_MEETINGRESPONSE_FOLDERID","MeetingResponse:FolderId"); +define("SYNC_MEETINGRESPONSE_MEETINGRESPONSE","MeetingResponse:MeetingResponse"); +define("SYNC_MEETINGRESPONSE_REQUESTID","MeetingResponse:RequestId"); +define("SYNC_MEETINGRESPONSE_REQUEST","MeetingResponse:Request"); +define("SYNC_MEETINGRESPONSE_RESULT","MeetingResponse:Result"); +define("SYNC_MEETINGRESPONSE_STATUS","MeetingResponse:Status"); +define("SYNC_MEETINGRESPONSE_USERRESPONSE","MeetingResponse:UserResponse"); +define("SYNC_MEETINGRESPONSE_VERSION","MeetingResponse:Version"); + +// POOMTASKS +define("SYNC_POOMTASKS_BODY","POOMTASKS:Body"); +define("SYNC_POOMTASKS_BODYSIZE","POOMTASKS:BodySize"); +define("SYNC_POOMTASKS_BODYTRUNCATED","POOMTASKS:BodyTruncated"); +define("SYNC_POOMTASKS_CATEGORIES","POOMTASKS:Categories"); +define("SYNC_POOMTASKS_CATEGORY","POOMTASKS:Category"); +define("SYNC_POOMTASKS_COMPLETE","POOMTASKS:Complete"); +define("SYNC_POOMTASKS_DATECOMPLETED","POOMTASKS:DateCompleted"); +define("SYNC_POOMTASKS_DUEDATE","POOMTASKS:DueDate"); +define("SYNC_POOMTASKS_UTCDUEDATE","POOMTASKS:UtcDueDate"); +define("SYNC_POOMTASKS_IMPORTANCE","POOMTASKS:Importance"); +define("SYNC_POOMTASKS_RECURRENCE","POOMTASKS:Recurrence"); +define("SYNC_POOMTASKS_TYPE","POOMTASKS:Type"); +define("SYNC_POOMTASKS_START","POOMTASKS:Start"); +define("SYNC_POOMTASKS_UNTIL","POOMTASKS:Until"); +define("SYNC_POOMTASKS_OCCURRENCES","POOMTASKS:Occurrences"); +define("SYNC_POOMTASKS_INTERVAL","POOMTASKS:Interval"); +define("SYNC_POOMTASKS_DAYOFWEEK","POOMTASKS:DayOfWeek"); +define("SYNC_POOMTASKS_DAYOFMONTH","POOMTASKS:DayOfMonth"); +define("SYNC_POOMTASKS_WEEKOFMONTH","POOMTASKS:WeekOfMonth"); +define("SYNC_POOMTASKS_MONTHOFYEAR","POOMTASKS:MonthOfYear"); +define("SYNC_POOMTASKS_REGENERATE","POOMTASKS:Regenerate"); +define("SYNC_POOMTASKS_DEADOCCUR","POOMTASKS:DeadOccur"); +define("SYNC_POOMTASKS_REMINDERSET","POOMTASKS:ReminderSet"); +define("SYNC_POOMTASKS_REMINDERTIME","POOMTASKS:ReminderTime"); +define("SYNC_POOMTASKS_SENSITIVITY","POOMTASKS:Sensitivity"); +define("SYNC_POOMTASKS_STARTDATE","POOMTASKS:StartDate"); +define("SYNC_POOMTASKS_UTCSTARTDATE","POOMTASKS:UtcStartDate"); +define("SYNC_POOMTASKS_SUBJECT","POOMTASKS:Subject"); +define("SYNC_POOMTASKS_RTF","POOMTASKS:Rtf"); + +// ResolveRecipients +define("SYNC_RESOLVERECIPIENTS_RESOLVERECIPIENTS","ResolveRecipients:ResolveRecipients"); +define("SYNC_RESOLVERECIPIENTS_RESPONSE","ResolveRecipients:Response"); +define("SYNC_RESOLVERECIPIENTS_STATUS","ResolveRecipients:Status"); +define("SYNC_RESOLVERECIPIENTS_TYPE","ResolveRecipients:Type"); +define("SYNC_RESOLVERECIPIENTS_RECIPIENT","ResolveRecipients:Recipient"); +define("SYNC_RESOLVERECIPIENTS_DISPLAYNAME","ResolveRecipients:DisplayName"); +define("SYNC_RESOLVERECIPIENTS_EMAILADDRESS","ResolveRecipients:EmailAddress"); +define("SYNC_RESOLVERECIPIENTS_CERTIFICATES","ResolveRecipients:Certificates"); +define("SYNC_RESOLVERECIPIENTS_CERTIFICATE","ResolveRecipients:Certificate"); +define("SYNC_RESOLVERECIPIENTS_MINICERTIFICATE","ResolveRecipients:MiniCertificate"); +define("SYNC_RESOLVERECIPIENTS_OPTIONS","ResolveRecipients:Options"); +define("SYNC_RESOLVERECIPIENTS_TO","ResolveRecipients:To"); +define("SYNC_RESOLVERECIPIENTS_CERTIFICATERETRIEVAL","ResolveRecipients:CertificateRetrieval"); +define("SYNC_RESOLVERECIPIENTS_RECIPIENTCOUNT","ResolveRecipients:RecipientCount"); +define("SYNC_RESOLVERECIPIENTS_MAXCERTIFICATES","ResolveRecipients:MaxCertificates"); +define("SYNC_RESOLVERECIPIENTS_MAXAMBIGUOUSRECIPIENTS","ResolveRecipients:MaxAmbiguousRecipients"); +define("SYNC_RESOLVERECIPIENTS_CERTIFICATECOUNT","ResolveRecipients:CertificateCount"); + +// ValidateCert +define("SYNC_VALIDATECERT_VALIDATECERT","ValidateCert:ValidateCert"); +define("SYNC_VALIDATECERT_CERTIFICATES","ValidateCert:Certificates"); +define("SYNC_VALIDATECERT_CERTIFICATE","ValidateCert:Certificate"); +define("SYNC_VALIDATECERT_CERTIFICATECHAIN","ValidateCert:CertificateChain"); +define("SYNC_VALIDATECERT_CHECKCRL","ValidateCert:CheckCRL"); +define("SYNC_VALIDATECERT_STATUS","ValidateCert:Status"); + +// POOMCONTACTS2 +define("SYNC_POOMCONTACTS2_CUSTOMERID","POOMCONTACTS2:CustomerId"); +define("SYNC_POOMCONTACTS2_GOVERNMENTID","POOMCONTACTS2:GovernmentId"); +define("SYNC_POOMCONTACTS2_IMADDRESS","POOMCONTACTS2:IMAddress"); +define("SYNC_POOMCONTACTS2_IMADDRESS2","POOMCONTACTS2:IMAddress2"); +define("SYNC_POOMCONTACTS2_IMADDRESS3","POOMCONTACTS2:IMAddress3"); +define("SYNC_POOMCONTACTS2_MANAGERNAME","POOMCONTACTS2:ManagerName"); +define("SYNC_POOMCONTACTS2_COMPANYMAINPHONE","POOMCONTACTS2:CompanyMainPhone"); +define("SYNC_POOMCONTACTS2_ACCOUNTNAME","POOMCONTACTS2:AccountName"); +define("SYNC_POOMCONTACTS2_NICKNAME","POOMCONTACTS2:NickName"); +define("SYNC_POOMCONTACTS2_MMS","POOMCONTACTS2:MMS"); + +// Ping +define("SYNC_PING_PING","Ping:Ping"); +define("SYNC_PING_STATUS","Ping:Status"); +define("SYNC_PING_LIFETIME", "Ping:LifeTime"); +define("SYNC_PING_FOLDERS", "Ping:Folders"); +define("SYNC_PING_FOLDER", "Ping:Folder"); +define("SYNC_PING_SERVERENTRYID", "Ping:ServerEntryId"); +define("SYNC_PING_FOLDERTYPE", "Ping:FolderType"); + +//Provision +define("SYNC_PROVISION_PROVISION", "Provision:Provision"); +define("SYNC_PROVISION_POLICIES", "Provision:Policies"); +define("SYNC_PROVISION_POLICY", "Provision:Policy"); +define("SYNC_PROVISION_POLICYTYPE", "Provision:PolicyType"); +define("SYNC_PROVISION_POLICYKEY", "Provision:PolicyKey"); +define("SYNC_PROVISION_DATA", "Provision:Data"); +define("SYNC_PROVISION_STATUS", "Provision:Status"); +define("SYNC_PROVISION_REMOTEWIPE", "Provision:RemoteWipe"); +define("SYNC_PROVISION_EASPROVISIONDOC", "Provision:EASProvisionDoc"); + +//Search +define("SYNC_SEARCH_SEARCH", "Search:Search"); +define("SYNC_SEARCH_STORE", "Search:Store"); +define("SYNC_SEARCH_NAME", "Search:Name"); +define("SYNC_SEARCH_QUERY", "Search:Query"); +define("SYNC_SEARCH_OPTIONS", "Search:Options"); +define("SYNC_SEARCH_RANGE", "Search:Range"); +define("SYNC_SEARCH_STATUS", "Search:Status"); +define("SYNC_SEARCH_RESPONSE", "Search:Response"); +define("SYNC_SEARCH_RESULT", "Search:Result"); +define("SYNC_SEARCH_PROPERTIES", "Search:Properties"); +define("SYNC_SEARCH_TOTAL", "Search:Total"); +define("SYNC_SEARCH_EQUALTO", "Search:EqualTo"); +define("SYNC_SEARCH_VALUE", "Search:Value"); +define("SYNC_SEARCH_AND", "Search:And"); +define("SYNC_SEARCH_OR", "Search:Or"); +define("SYNC_SEARCH_FREETEXT", "Search:FreeText"); +define("SYNC_SEARCH_DEEPTRAVERSAL", "Search:DeepTraversal"); +define("SYNC_SEARCH_LONGID", "Search:LongId"); +define("SYNC_SEARCH_REBUILDRESULTS", "Search:RebuildResults"); +define("SYNC_SEARCH_LESSTHAN", "Search:LessThan"); +define("SYNC_SEARCH_GREATERTHAN", "Search:GreaterThan"); +define("SYNC_SEARCH_SCHEMA", "Search:Schema"); +define("SYNC_SEARCH_SUPPORTED", "Search:Supported"); + +//GAL +define("SYNC_GAL_DISPLAYNAME", "GAL:DisplayName"); +define("SYNC_GAL_PHONE", "GAL:Phone"); +define("SYNC_GAL_OFFICE", "GAL:Office"); +define("SYNC_GAL_TITLE", "GAL:Title"); +define("SYNC_GAL_COMPANY", "GAL:Company"); +define("SYNC_GAL_ALIAS", "GAL:Alias"); +define("SYNC_GAL_FIRSTNAME", "GAL:FirstName"); +define("SYNC_GAL_LASTNAME", "GAL:LastName"); +define("SYNC_GAL_HOMEPHONE", "GAL:HomePhone"); +define("SYNC_GAL_MOBILEPHONE", "GAL:MobilePhone"); +define("SYNC_GAL_EMAILADDRESS", "GAL:EmailAddress"); + +// Other constants +define("SYNC_FOLDER_TYPE_OTHER", 1); +define("SYNC_FOLDER_TYPE_INBOX", 2); +define("SYNC_FOLDER_TYPE_DRAFTS", 3); +define("SYNC_FOLDER_TYPE_WASTEBASKET", 4); +define("SYNC_FOLDER_TYPE_SENTMAIL", 5); +define("SYNC_FOLDER_TYPE_OUTBOX", 6); +define("SYNC_FOLDER_TYPE_TASK", 7); +define("SYNC_FOLDER_TYPE_APPOINTMENT", 8); +define("SYNC_FOLDER_TYPE_CONTACT", 9); +define("SYNC_FOLDER_TYPE_NOTE", 10); +define("SYNC_FOLDER_TYPE_JOURNAL", 11); +define("SYNC_FOLDER_TYPE_USER_MAIL", 12); +define("SYNC_FOLDER_TYPE_USER_APPOINTMENT", 13); +define("SYNC_FOLDER_TYPE_USER_CONTACT", 14); +define("SYNC_FOLDER_TYPE_USER_TASK", 15); +define("SYNC_FOLDER_TYPE_USER_JOURNAL", 16); +define("SYNC_FOLDER_TYPE_USER_NOTE", 17); +define("SYNC_FOLDER_TYPE_UNKNOWN", 18); +define("SYNC_FOLDER_TYPE_RECIPIENT_CACHE", 19); +define("SYNC_FOLDER_TYPE_DUMMY", "__dummy.Folder.Id__"); + +define("SYNC_CONFLICT_OVERWRITE_SERVER", 0); +define("SYNC_CONFLICT_OVERWRITE_PIM", 1); + +define("SYNC_TRUNCATION_HEADERS", 0); +define("SYNC_TRUNCATION_512B", 1); +define("SYNC_TRUNCATION_1K", 2); +define("SYNC_TRUNCATION_5K", 4); +define("SYNC_TRUNCATION_SEVEN", 7); +define("SYNC_TRUNCATION_ALL", 9); + +define("SYNC_PROVISION_STATUS_SUCCESS", 1); +define("SYNC_PROVISION_STATUS_PROTERROR", 2); +define("SYNC_PROVISION_STATUS_SERVERERROR", 3); +define("SYNC_PROVISION_STATUS_DEVEXTMANAGED", 4); +define("SYNC_PROVISION_STATUS_POLKEYMISM", 5); + +define("SYNC_PROVISION_RWSTATUS_NA", 0); +define("SYNC_PROVISION_RWSTATUS_OK", 1); +define("SYNC_PROVISION_RWSTATUS_PENDING", 2); +define("SYNC_PROVISION_RWSTATUS_WIPED", 3); + +/** + * Main ActiveSync class. Entry point for performing all ActiveSync operations + * + */ +class Horde_ActiveSync +{ + /* SYNC Status response codes */ + const STATUS_SYNC_SUCCESS = 1; + const STATUS_SYNC_VERSIONMISM = 2; + const STATUS_SYNC_KEYMISM = 3; + const STATUS_SYNC_PROTERROR = 4; + const STATUS_SYNC_SERVERERROR = 5; + + const STATUS_PING_NOCHANGES = 1; + const STATUS_PING_NEEDSYNC = 2; + const STATUS_PING_MISSING = 3; + const STATUS_PING_PROTERROR = 4; + // Hearbeat out of bounds (TODO) + const STATUS_PING_HBOUTOFBOUNDS = 5; + + // Requested more then the max folders (TODO) + const STATUS_PING_MAXFOLDERS = 6; + + // Folder sync is required, hierarchy out of date. + const STATUS_PING_FOLDERSYNCREQD = 7; + const STATUS_PING_SERVERERROR = 8; + + + /** + * DTD + */ + static public $zpushdtd = array( + "codes" => array ( + 0 => array ( + 0x05 => "Synchronize", + 0x06 => "Replies", + 0x07 => "Add", + 0x08 => "Modify", + 0x09 => "Remove", + 0x0a => "Fetch", + 0x0b => "SyncKey", + 0x0c => "ClientEntryId", + 0x0d => "ServerEntryId", + 0x0e => "Status", + 0x0f => "Folder", + 0x10 => "FolderType", + 0x11 => "Version", + 0x12 => "FolderId", + 0x13 => "GetChanges", + 0x14 => "MoreAvailable", + 0x15 => "WindowSize", + 0x16 => "Commands", + 0x17 => "Options", + 0x18 => "FilterType", + 0x19 => "Truncation", + 0x1a => "RtfTruncation", + 0x1b => "Conflict", + 0x1c => "Folders", + 0x1d => "Data", + 0x1e => "DeletesAsMoves", + 0x1f => "NotifyGUID", + 0x20 => "Supported", + 0x21 => "SoftDelete", + 0x22 => "MIMESupport", + 0x23 => "MIMETruncation", + ), + 1 => array ( + 0x05 => "Anniversary", + 0x06 => "AssistantName", + 0x07 => "AssistnamePhoneNumber", + 0x08 => "Birthday", + 0x09 => "Body", + 0x0a => "BodySize", + 0x0b => "BodyTruncated", + 0x0c => "Business2PhoneNumber", + 0x0d => "BusinessCity", + 0x0e => "BusinessCountry", + 0x0f => "BusinessPostalCode", + 0x10 => "BusinessState", + 0x11 => "BusinessStreet", + 0x12 => "BusinessFaxNumber", + 0x13 => "BusinessPhoneNumber", + 0x14 => "CarPhoneNumber", + 0x15 => "Categories", + 0x16 => "Category", + 0x17 => "Children", + 0x18 => "Child", + 0x19 => "CompanyName", + 0x1a => "Department", + 0x1b => "Email1Address", + 0x1c => "Email2Address", + 0x1d => "Email3Address", + 0x1e => "FileAs", + 0x1f => "FirstName", + 0x20 => "Home2PhoneNumber", + 0x21 => "HomeCity", + 0x22 => "HomeCountry", + 0x23 => "HomePostalCode", + 0x24 => "HomeState", + 0x25 => "HomeStreet", + 0x26 => "HomeFaxNumber", + 0x27 => "HomePhoneNumber", + 0x28 => "JobTitle", + 0x29 => "LastName", + 0x2a => "MiddleName", + 0x2b => "MobilePhoneNumber", + 0x2c => "OfficeLocation", + 0x2d => "OtherCity", + 0x2e => "OtherCountry", + 0x2f => "OtherPostalCode", + 0x30 => "OtherState", + 0x31 => "OtherStreet", + 0x32 => "PagerNumber", + 0x33 => "RadioPhoneNumber", + 0x34 => "Spouse", + 0x35 => "Suffix", + 0x36 => "Title", + 0x37 => "WebPage", + 0x38 => "YomiCompanyName", + 0x39 => "YomiFirstName", + 0x3a => "YomiLastName", + 0x3b => "Rtf", + 0x3c => "Picture", + ), + 2 => array ( + 0x05 => "Attachment", + 0x06 => "Attachments", + 0x07 => "AttName", + 0x08 => "AttSize", + 0x09 => "AttOid", + 0x0a => "AttMethod", + 0x0b => "AttRemoved", + 0x0c => "Body", + 0x0d => "BodySize", + 0x0e => "BodyTruncated", + 0x0f => "DateReceived", + 0x10 => "DisplayName", + 0x11 => "DisplayTo", + 0x12 => "Importance", + 0x13 => "MessageClass", + 0x14 => "Subject", + 0x15 => "Read", + 0x16 => "To", + 0x17 => "Cc", + 0x18 => "From", + 0x19 => "Reply-To", + 0x1a => "AllDayEvent", + 0x1b => "Categories", + 0x1c => "Category", + 0x1d => "DtStamp", + 0x1e => "EndTime", + 0x1f => "InstanceType", + 0x20 => "BusyStatus", + 0x21 => "Location", + 0x22 => "MeetingRequest", + 0x23 => "Organizer", + 0x24 => "RecurrenceId", + 0x25 => "Reminder", + 0x26 => "ResponseRequested", + 0x27 => "Recurrences", + 0x28 => "Recurrence", + 0x29 => "Type", + 0x2a => "Until", + 0x2b => "Occurrences", + 0x2c => "Interval", + 0x2d => "DayOfWeek", + 0x2e => "DayOfMonth", + 0x2f => "WeekOfMonth", + 0x30 => "MonthOfYear", + 0x31 => "StartTime", + 0x32 => "Sensitivity", + 0x33 => "TimeZone", + 0x34 => "GlobalObjId", + 0x35 => "ThreadTopic", + 0x36 => "MIMEData", + 0x37 => "MIMETruncated", + 0x38 => "MIMESize", + 0x39 => "InternetCPID", + ), + 3 => array ( + 0x05 => "Notify", + 0x06 => "Notification", + 0x07 => "Version", + 0x08 => "Lifetime", + 0x09 => "DeviceInfo", + 0x0a => "Enable", + 0x0b => "Folder", + 0x0c => "ServerEntryId", + 0x0d => "DeviceAddress", + 0x0e => "ValidCarrierProfiles", + 0x0f => "CarrierProfile", + 0x10 => "Status", + 0x11 => "Replies", +// 0x05 => "Version='1.1'", + 0x12 => "Devices", + 0x13 => "Device", + 0x14 => "Id", + 0x15 => "Expiry", + 0x16 => "NotifyGUID", + ), + 4 => array ( + 0x05 => "Timezone", + 0x06 => "AllDayEvent", + 0x07 => "Attendees", + 0x08 => "Attendee", + 0x09 => "Email", + 0x0a => "Name", + 0x0b => "Body", + 0x0c => "BodyTruncated", + 0x0d => "BusyStatus", + 0x0e => "Categories", + 0x0f => "Category", + 0x10 => "Rtf", + 0x11 => "DtStamp", + 0x12 => "EndTime", + 0x13 => "Exception", + 0x14 => "Exceptions", + 0x15 => "Deleted", + 0x16 => "ExceptionStartTime", + 0x17 => "Location", + 0x18 => "MeetingStatus", + 0x19 => "OrganizerEmail", + 0x1a => "OrganizerName", + 0x1b => "Recurrence", + 0x1c => "Type", + 0x1d => "Until", + 0x1e => "Occurrences", + 0x1f => "Interval", + 0x20 => "DayOfWeek", + 0x21 => "DayOfMonth", + 0x22 => "WeekOfMonth", + 0x23 => "MonthOfYear", + 0x24 => "Reminder", + 0x25 => "Sensitivity", + 0x26 => "Subject", + 0x27 => "StartTime", + 0x28 => "UID", + 0x36 => "ResponseType" + ), 5 => array ( + 0x05 => "Moves", + 0x06 => "Move", + 0x07 => "SrcMsgId", + 0x08 => "SrcFldId", + 0x09 => "DstFldId", + 0x0a => "Response", + 0x0b => "Status", + 0x0c => "DstMsgId", + ), 6 => array ( + 0x05 => "GetItemEstimate", + 0x06 => "Version", + 0x07 => "Folders", + 0x08 => "Folder", + 0x09 => "FolderType", + 0x0a => "FolderId", + 0x0b => "DateTime", + 0x0c => "Estimate", + 0x0d => "Response", + 0x0e => "Status", + ), 7 => array ( + 0x05 => "Folders", + 0x06 => "Folder", + 0x07 => "DisplayName", + 0x08 => "ServerEntryId", + 0x09 => "ParentId", + 0x0a => "Type", + 0x0b => "Response", + 0x0c => "Status", + 0x0d => "ContentClass", + 0x0e => "Changes", + 0x0f => "Add", + 0x10 => "Remove", + 0x11 => "Update", + 0x12 => "SyncKey", + 0x13 => "FolderCreate", + 0x14 => "FolderDelete", + 0x15 => "FolderUpdate", + 0x16 => "FolderSync", + 0x17 => "Count", + 0x18 => "Version", + ), 8 => array ( + 0x05 => "CalendarId", + 0x06 => "FolderId", + 0x07 => "MeetingResponse", + 0x08 => "RequestId", + 0x09 => "Request", + 0x0a => "Result", + 0x0b => "Status", + 0x0c => "UserResponse", + 0x0d => "Version", + ), 9 => array ( + 0x05 => "Body", + 0x06 => "BodySize", + 0x07 => "BodyTruncated", + 0x08 => "Categories", + 0x09 => "Category", + 0x0a => "Complete", + 0x0b => "DateCompleted", + 0x0c => "DueDate", + 0x0d => "UtcDueDate", + 0x0e => "Importance", + 0x0f => "Recurrence", + 0x10 => "Type", + 0x11 => "Start", + 0x12 => "Until", + 0x13 => "Occurrences", + 0x14 => "Interval", + 0x16 => "DayOfWeek", + 0x15 => "DayOfMonth", + 0x17 => "WeekOfMonth", + 0x18 => "MonthOfYear", + 0x19 => "Regenerate", + 0x1a => "DeadOccur", + 0x1b => "ReminderSet", + 0x1c => "ReminderTime", + 0x1d => "Sensitivity", + 0x1e => "StartDate", + 0x1f => "UtcStartDate", + 0x20 => "Subject", + 0x21 => "Rtf", + ), 0xa => array ( + 0x05 => "ResolveRecipients", + 0x06 => "Response", + 0x07 => "Status", + 0x08 => "Type", + 0x09 => "Recipient", + 0x0a => "DisplayName", + 0x0b => "EmailAddress", + 0x0c => "Certificates", + 0x0d => "Certificate", + 0x0e => "MiniCertificate", + 0x0f => "Options", + 0x10 => "To", + 0x11 => "CertificateRetrieval", + 0x12 => "RecipientCount", + 0x13 => "MaxCertificates", + 0x14 => "MaxAmbiguousRecipients", + 0x15 => "CertificateCount", + ), 0xb => array ( + 0x05 => "ValidateCert", + 0x06 => "Certificates", + 0x07 => "Certificate", + 0x08 => "CertificateChain", + 0x09 => "CheckCRL", + 0x0a => "Status", + ), 0xc => array ( + 0x05 => "CustomerId", + 0x06 => "GovernmentId", + 0x07 => "IMAddress", + 0x08 => "IMAddress2", + 0x09 => "IMAddress3", + 0x0a => "ManagerName", + 0x0b => "CompanyMainPhone", + 0x0c => "AccountName", + 0x0d => "NickName", + 0x0e => "MMS", + ), 0xd => array ( + 0x05 => "Ping", + 0x07 => "Status", + 0x08 => "LifeTime", + 0x09 => "Folders", + 0x0a => "Folder", + 0x0b => "ServerEntryId", + 0x0c => "FolderType", + ), 0xe => array ( + 0x05 => "Provision", + 0x06 => "Policies", + 0x07 => "Policy", + 0x08 => "PolicyType", + 0x09 => "PolicyKey", + 0x0A => "Data", + 0x0B => "Status", + 0x0C => "RemoteWipe", + 0x0D => "EASProvisionDoc", + ), + 0xf => array( + 0x05 => "Search", + 0x07 => "Store", + 0x08 => "Name", + 0x09 => "Query", + 0x0A => "Options", + 0x0B => "Range", + 0x0C => "Status", + 0x0D => "Response", + 0x0E => "Result", + 0x0F => "Properties", + 0x10 => "Total", + 0x11 => "EqualTo", + 0x12 => "Value", + 0x13 => "And", + 0x14 => "Or", + 0x15 => "FreeText", + 0x17 => "DeepTraversal", + 0x18 => "LongId", + 0x19 => "RebuildResults", + 0x1A => "LessThan", + 0x1B => "GreaterThan", + 0x1C => "Schema", + 0x1D => "Supported", + ), 0x10 => array( + 0x05 => "DisplayName", + 0x06 => "Phone", + 0x07 => "Office", + 0x08 => "Title", + 0x09 => "Company", + 0x0A => "Alias", + 0x0B => "FirstName", + 0x0C => "LastName", + 0x0D => "HomePhone", + 0x0E => "MobilePhone", + 0x0F => "EmailAddress", + ) + ), "namespaces" => array( + 1 => "POOMCONTACTS", + 2 => "POOMMAIL", + 3 => "AirNotify", + 4 => "POOMCAL", + 5 => "Move", + 6 => "GetItemEstimate", + 7 => "FolderHierarchy", + 8 => "MeetingResponse", + 9 => "POOMTASKS", + 0xA => "ResolveRecipients", + 0xB => "ValidateCerts", + 0xC => "POOMCONTACTS2", + 0xD => "Ping", + 0xE => "Provision",// + 0xF => "Search",// + 0x10 => "GAL", + ) + ); + + /** + * Used to track what error code to send back to PIM on failure + * + * @var integer + */ + protected $_statusCode = 0; + + protected $_provisioning; + + /** + * Const'r + * + * @param Horde_ActiveSync_Driver $driver The backend driver + * @param Horde_ActiveSync_StateMachine $state The state machine + * @param Horde_ActiveSync_Wbxml_Decoder $decoder The Wbxml decoder + * @param Horde_ActiveSync_Wbxml_Endcodder $encdoer The Wbxml encoder + * + * @return Horde_ActiveSync + */ + public function __construct(Horde_ActiveSync_Driver_Base $driver, + Horde_ActiveSync_Wbxml_Decoder $decoder, + Horde_ActiveSync_Wbxml_Encoder $encoder, + Horde_Controller_Request_Http $request) + { + /* Backend driver */ + $this->_driver = $driver; + + /* Wbxml handlers */ + $this->_encoder = $encoder; + $this->_decoder = $decoder; + + /* The http request */ + $this->_request = $request; + } + + /** + * Setter for the logger + * + * @param Horde_Log_Logger $logger The logger object. + * + * @return void + */ + public function setLogger(Horde_Log_Logger $logger) + { + $this->_logger = $logger; + $this->_encoder->setLogger($logger); + $this->_decoder->setLogger($logger); + $this->_driver->setLogger($logger); + } + + /** + * Setter for provisioning support + * + */ + public function setProvisioning($provision) + { + $this->_provisioning = $provision; + } + + /** + * + * @param $protocolversion + * + * @return true + */ + public function handleMoveItems($protocolversion) + { + if (!$this->_decoder->getElementStartTag(SYNC_MOVE_MOVES)) { + return false; + } + + $moves = array(); + while ($this->_decoder->getElementStartTag(SYNC_MOVE_MOVE)) { + $move = array(); + if ($this->_decoder->getElementStartTag(SYNC_MOVE_SRCMSGID)) { + $move['srcmsgid'] = $this->_decoder->getElementContent(); + if(!$this->_decoder->getElementEndTag()) + break; + } + if ($this->_decoder->getElementStartTag(SYNC_MOVE_SRCFLDID)) { + $move['srcfldid'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + break; + } + } + if ($this->_decoder->getElementStartTag(SYNC_MOVE_DSTFLDID)) { + $move['dstfldid'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + break; + } + } + array_push($moves, $move); + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + } + + if (!$this->_decoder->getElementEndTag()) + return false; + + $this->_encoder->StartWBXML(); + + $this->_encoder->startTag(SYNC_MOVE_MOVES); + + foreach ($moves as $move) { + $this->_encoder->startTag(SYNC_MOVE_RESPONSE); + $this->_encoder->startTag(SYNC_MOVE_SRCMSGID); + $this->_encoder->content($move['srcmsgid']); + $this->_encoder->endTag(); + + $importer = $this->_driver->GetContentsImporter($move['srcfldid']); + $result = $importer->ImportMessageMove($move['srcmsgid'], $move['dstfldid']); + + // We discard the importer state for now. + $this->_encoder->startTag(SYNC_MOVE_STATUS); + $this->_encoder->content($result ? 3 : 1); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_MOVE_DSTMSGID); + $this->_encoder->content(is_string($result) ? $result : $move['srcmsgid']); + $this->_encoder->endTag(); + $this->_encoder->endTzg(); + } + $this->_encoder->endTag(); + + return true; + } + + /** + * @param $protocolversion + * + * @return boolean + */ + public function handleNotify($protocolversion) + { + if (!$this->_decoder->getElementStartTag(SYNC_AIRNOTIFY_NOTIFY)) { + return false; + } + + if (!$this->_decoder->getElementStartTag(SYNC_AIRNOTIFY_DEVICEINFO)) { + return false; + } + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + $this->_encoder->StartWBXML(); + $this->_encoder->startTag(SYNC_AIRNOTIFY_NOTIFY); + $this->_encoder->startTag(SYNC_AIRNOTIFY_STATUS); + $this->_encoder->content(1); + $this->_encoder->endTag(); + $this->_encoder->startTag(SYNC_AIRNOTIFY_VALIDCARRIERPROFILES); + $this->_encoder->endTag(); + $this->_encoder->endTag(); + + return true; + } + + /** + * handle GetHierarchy method - simply returns current hierarchy of all + * folders + * + * @param string $protocolversion + * @param string $devid + * + * @return boolean + */ + public function handleGetHierarchy($protocolversion, $devid) + { + $folders = $this->_driver->GetHierarchy(); + if (!$folders) { + return false; + } + + // save folder-ids for fourther syncing + $this->_stateMachine->setFolderData($devid, $folders); + + $this->_encoder->StartWBXML(); + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERS); + + foreach ($folders as $folder) { + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDER); + $folder->encodeStream($this->_encoder); + $this->_encoder->endTag(); + } + $this->_encoder->endTag(); + + return true; + } + + /** + * + * @param $protocolversion + * @param $devid + * @return unknown_type + */ + public function handleGetItemEstimate($protocolversion, $devid) + { + $collections = array(); + + if (!$this->_decoder->getElementStartTag(SYNC_GETITEMESTIMATE_GETITEMESTIMATE)) { + return false; + } + + if (!$this->_decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERS)) { + return false; + } + + while ($this->_decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDER)) { + $collection = array(); + + if (!$this->_decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERTYPE)) { + return false; + } + + $class = $this->_decoder->getElementContent(); + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + if ($this->_decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERID)) { + $collectionid = $this->_decoder->getElementContent(); + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + } + + if (!$this->_decoder->getElementStartTag(SYNC_FILTERTYPE)) { + return false; + } + $filtertype = $this->_decoder->getElementContent(); + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + if (!$this->_decoder->getElementStartTag(SYNC_SYNCKEY)) { + return false; + } + + $synckey = $this->_decoder->getElementContent(); + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + // compatibility mode - get folderid from the state directory + if (!isset($collectionid)) { + $collectionid = $this->_stateMachine->getFolderData($devid, $class); + } + + $collection = array(); + $collection['synckey'] = $synckey; + $collection['class'] = $class; + $collection['filtertype'] = $filtertype; + $collection['collectionid'] = $collectionid; + + array_push($collections, $collection); + } + + $this->_encoder->startWBXML(); + + $this->_encoder->startTag(SYNC_GETITEMESTIMATE_GETITEMESTIMATE); + foreach ($collections as $collection) { + $this->_encoder->startTag(SYNC_GETITEMESTIMATE_RESPONSE); + + $this->_encoder->startTag(SYNC_GETITEMESTIMATE_STATUS); + $this->_encoder->content(1); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_GETITEMESTIMATE_FOLDER); + + $this->_encoder->startTag(SYNC_GETITEMESTIMATE_FOLDERTYPE); + $this->_encoder->content($collection['class']); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_GETITEMESTIMATE_FOLDERID); + $this->_encoder->content($collection['collectionid']); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_GETITEMESTIMATE_ESTIMATE); + + $importer = new Horde_ActiveSync_ContentsCache(); + + $syncstate = $this->_stateMachine->loadState($collection['synckey']); + + $exporter = $this->_driver->GetExporter($collection['collectionid']); + $exporter->Config($importer, $collection['class'], $collection['filtertype'], $syncstate, 0, 0); + + $this->_encoder->content($exporter->GetChangeCount()); + + $this->_encoder->endTag(); + + $this->_encoder->endTag(); + + $this->_encoder->endTag(); + } + + $this->_encoder->endTag(); + + return true; + } + + /** + * @param $protocolversion + * @return unknown_type + */ + public function handleGetAttachment($protocolversion) + { + $get = $this->_request->getGetParams(); + $attname = $get('AttachmentName'); + if (!isset($attname)) { + return false; + } + + header("Content-Type: application/octet-stream"); + $this->_driver->GetAttachmentData($attname); + + return true; + } + + /** + * + * @param $protocolversion + * @return unknown_type + */ + public function handleSendMail($protocolversion) + { + // All that happens here is that we receive an rfc822 message on stdin + // and just forward it to the backend. We provide no output except for + // an OK http reply + $rfc822 = $this->readStream(); + + return $this->_driver->SendMail($rfc822); + } + + /** + * + * @param $protocolversion + * @return unknown_type + */ + public function handleSmartForward($protocolversion) + { + // SmartForward is a normal 'send' except that you should attach the + // original message which is specified in the URL + + $rfc822 = $this->readStream(); + + if (isset($_GET["ItemId"])) { + $orig = $_GET["ItemId"]; + } else { + $orig = false; + } + if (isset($_GET["CollectionId"])) { + $parent = $_GET["CollectionId"]; + } else { + $parent = false; + } + + return $this->_driver->SendMail($rfc822, $orig, false, $parent); + } + + /** + * @TODO: use Horde_Controller_Request_Http for the GET + * + * @param unknown_type $protocolversion + * @return unknown_type + */ + public function handleSmartReply($protocolversion) + { + // Smart reply should add the original message to the end of the message body + $rfc822 = $this->readStream(); + + if (isset($_GET["ItemId"])) { + $orig = $_GET["ItemId"]; + } else { + $orig = false; + } + + if (isset($_GET["CollectionId"])) { + $parent = $_GET["CollectionId"]; + } else { + $parent = false; + } + + return $this->_driver->SendMail($rfc822, false, $orig, $parent); + } + + /** + * @param $protocolversion + * @return unknown_type + */ + public function handleFolderCreate($protocolversion) + { + $el = $this->_decoder->getElement(); + if ($el[Horde_ActiveSync_Wbxml::EN_TYPE] != Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG) { + return false; + } + + $create = $update = $delete = false; + + if ($el[Horde_ActiveSync_Wbxml::EN_TAG] == SYNC_FOLDERHIERARCHY_FOLDERCREATE) { + $create = true; + } elseif ($el[Horde_ActiveSync_Wbxml::EN_TAG] == SYNC_FOLDERHIERARCHY_FOLDERUPDATE) { + $update = true; + } elseif ($el[Horde_ActiveSync_Wbxml::EN_TAG] == SYNC_FOLDERHIERARCHY_FOLDERDELETE) { + $delete = true; + } + + if (!$create && !$update && !$delete) { + return false; + } + + // SyncKey + if (!$this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SYNCKEY)) { + return false; + } + $synckey = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + // ServerID + $serverid = false; + if ($this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SERVERENTRYID)) { + $serverid = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + } + + // when creating or updating more information is necessary + if (!$delete) { + // Parent + $parentid = false; + if ($this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_PARENTID)) { + $parentid = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + } + + // Displayname + if (!$this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_DISPLAYNAME)) { + return false; + } + $displayname = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + // Type + $type = false; + if ($this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_TYPE)) { + $type = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + } + } + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + // Get state of hierarchy + $syncstate = $this->_stateMachine->loadState($synckey); + $newsynckey = $this->_stateMachine->getNewSyncKey($synckey); + + // additional information about already seen folders + $seenfolders = unserialize($this->_stateMachine->loadState('s' . $synckey)); + if (!$seenfolders) { + $seenfolders = array(); + } + // Configure importer with last state + $importer = $this->_driver->GetHierarchyImporter(); + $importer->Config($syncstate); + + if (!$delete) { + // Send change + $serverid = $importer->ImportFolderChange($serverid, $parentid, $displayname, $type); + } else { + // delete folder + $deletedstat = $importer->ImportFolderDeletion($serverid, 0); + } + + $this->_encoder->startWBXML(); + if ($create) { + // add folder id to the seen folders + $seenfolders[] = $serverid; + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERCREATE); + + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); + $this->_encoder->content(1); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); + $this->_encoder->content($newsynckey); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_SERVERENTRYID); + $this->_encoder->content($serverid); + $this->_encoder->endTag(); + + $this->_encoder->endTag(); + + $this->_encoder->endTag(); + } elseif ($update) { + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERUPDATE); + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); + $this->_encoder->content(1); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); + $this->_encoder->content($newsynckey); + $this->_encoder->endTag(); + + $this->_encoder->endTag(); + } elseif ($delete) { + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERDELETE); + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); + $this->_encoder->content($deletedstat); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); + $this->_encoder->content($newsynckey); + $this->_encoder->endTag(); + + $this->_encoder->endTag(); + + // remove folder from the folderflags array + if (($sid = array_search($serverid, $seenfolders)) !== false) { + unset($seenfolders[$sid]); + $seenfolders = array_values($seenfolders); + $this->_logger->debug('Deleted from seenfolders: ' . $serverid); + } + } + + $this->_encoder->endTag(); + // Save the sync state for the next time + $this->_stateMachine->setState($newsynckey, $importer->GetState()); + $this->_stateMachine->setState('s' . $newsynckey, serialize($seenfolders)); + $this->_stateMachine->save(); + + return true; + } + + /** + * handle meetingresponse method + */ + public function handleMeetingResponse($protocolversion) + { + $requests = Array(); + if (!$this->_decoder->getElementStartTag(SYNC_MEETINGRESPONSE_MEETINGRESPONSE)) { + return false; + } + + while ($this->_decoder->getElementStartTag(SYNC_MEETINGRESPONSE_REQUEST)) { + $req = Array(); + + if ($this->_decoder->getElementStartTag(SYNC_MEETINGRESPONSE_USERRESPONSE)) { + $req['response'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + } + + if ($this->_decoder->getElementStartTag(SYNC_MEETINGRESPONSE_FOLDERID)) { + $req['folderid'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + } + + if ($this->_decoder->getElementStartTag(SYNC_MEETINGRESPONSE_REQUESTID)) { + $req['requestid'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + } + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + array_push($requests, $req); + } + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + // Start output, simply the error code, plus the ID of the calendar item that was generated by the + // accept of the meeting response + $this->_encoder->StartWBXML(); + $this->_encoder->startTag(SYNC_MEETINGRESPONSE_MEETINGRESPONSE); + + foreach ($requests as $req) { + $calendarid = ''; + $ok = $this->_driver->MeetingResponse($req['requestid'], $req['folderid'], $req['response'], $calendarid); + $this->_encoder->startTag(SYNC_MEETINGRESPONSE_RESULT); + $this->_encoder->startTag(SYNC_MEETINGRESPONSE_REQUESTID); + $this->_encoder->content($req['requestid']); + $this->_encoder->endTag(); + $this->_encoder->startTag(SYNC_MEETINGRESPONSE_STATUS); + $this->_encoder->content($ok ? 1 : 2); + $this->_encoder->endTag(); + if ($ok) { + $this->_encoder->startTag(SYNC_MEETINGRESPONSE_CALENDARID); + $this->_encoder->content($calendarid); + $this->_encoder->endTag(); + } + $this->_encoder->endTag(); + } + + $this->_encoder->endTag(); + + return true; + } + + + /** + * @param $protocolversion + * @return unknown_type + */ + public function handleFolderUpdate($protocolversion) + { + return $this->handleFolderCreate($protocolversion); + } + + /** + * + * @param $protocolversion + * @return unknown_type + */ + public function handleFolderDelete($protocolversion) { + return $this->handleFolderCreate($this->_driver, $protocolversion); + } + + public function provisioningRequired() + { + self::provisionHeader(); + self::activeSyncHeader(); + self::versionHeader(); + self::commandsHeader(); + header("Cache-Control: private"); + } + + /** + * @param $devid + * @param $protocolversion + * @return unknown_type + */ + public function handleSearch($devid, $protocolversion) + { + $searchrange = '0'; + if (!$this->_decoder->getElementStartTag(SYNC_SEARCH_SEARCH)) { + return false; + } + + if (!$this->_decoder->getElementStartTag(SYNC_SEARCH_STORE)) { + return false; + } + + if (!$this->_decoder->getElementStartTag(SYNC_SEARCH_NAME)) { + return false; + } + $searchname = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + if (!$this->_decoder->getElementStartTag(SYNC_SEARCH_QUERY)) { + return false; + } + $searchquery = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + if ($this->_decoder->getElementStartTag(SYNC_SEARCH_OPTIONS)) { + while(1) { + if ($this->_decoder->getElementStartTag(SYNC_SEARCH_RANGE)) { + $searchrange = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + } + $e = $this->_decoder->peek(); + if ($e[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) { + $this->_decoder->getElementEndTag(); + break; + } + } + } + if (!$this->_decoder->getElementEndTag()) {//store + return false; + } + + if (!$this->_decoder->getElementEndTag()) {//search + return false; + } + + if (strtoupper($searchname) != "GAL") { + $this->_logger->err('Searchtype ' . $searchname . 'is not supported'); + return false; + } + //get search results from backend + $rows = $this->_driver->getSearchResults($searchquery, $searchrange); + + $this->_encoder->startWBXML(); + $this->_encoder->startTag(SYNC_SEARCH_SEARCH); + + $this->_encoder->startTag(SYNC_SEARCH_STATUS); + $this->_encoder->content(1); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_SEARCH_RESPONSE); + $this->_encoder->startTag(SYNC_SEARCH_STORE); + + $this->_encoder->startTag(SYNC_SEARCH_STATUS); + $this->_encoder->content(1); + $this->_encoder->endTag(); + + if (is_array($rows) && !empty($rows)) { + $searchrange = $rows['range']; + unset($rows['range']); + foreach ($rows as $u) { + $this->_encoder->startTag(SYNC_SEARCH_RESULT); + $this->_encoder->startTag(SYNC_SEARCH_PROPERTIES); + + $this->_encoder->startTag(SYNC_GAL_DISPLAYNAME); + $this->_encoder->content($u["fullname"]); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_GAL_PHONE); + $this->_encoder->content($u["businessphone"]); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_GAL_ALIAS); + $this->_encoder->content($u["username"]); + $this->_encoder->endTag(); + + //it's not possible not get first and last name of an user + //from the gab and user functions, so we just set fullname + //to lastname and leave firstname empty because nokia needs + //first and lastname in order to display the search result + $this->_encoder->startTag(SYNC_GAL_FIRSTNAME); + $this->_encoder->content(""); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_GAL_LASTNAME); + $this->_encoder->content($u["fullname"]); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_GAL_EMAILADDRESS); + $this->_encoder->content($u["emailaddress"]); + $this->_encoder->endTag(); + + $this->_encoder->endTag();//result + $this->_encoder->endTag();//properties + } + $this->_encoder->startTag(SYNC_SEARCH_RANGE); + $this->_encoder->content($searchrange); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_SEARCH_TOTAL); + $this->_encoder->content(count($rows)); + $this->_encoder->endTag(); + } + + $this->_encoder->endTag();//store + $this->_encoder->endTag();//response + $this->_encoder->endTag();//search + + + return true; + } + + /** + * @param $cmd + * @param $devid + * @param $protocolversion + * @return unknown_type + */ + public function handleRequest($cmd, $devId, $version) + { + $class = 'Horde_ActiveSync_Request_' . basename($cmd); + if (class_exists($class)) { + $request = new $class($this->_driver, + $this->_decoder, + $this->_encoder, + $this->_request, + $version, + $devId, + $this->_provisioning); + $request->setLogger($this->_logger); + return $request->handle($this); + } + + // GetHierarchy is used in v1.0 of the AS protocol, in v2, it is replaced + // by the FolderSync command + + // @TODO: Leave the following in place until all are refactored...then throw + // an error if the class does not exist. + switch($cmd) { + case 'SendMail': + $status = $this->handleSendMail($version); + break; + case 'SmartForward': + $status = $this->handleSmartForward($version); + break; + case 'SmartReply': + $status = $this->handleSmartReply($version); + break; + case 'GetAttachment': + $status = $this->handleGetAttachment($version); + break; + case 'GetHierarchy': + $status = $this->handleGetHierarchy($version, $devId); + break; + case 'CreateCollection': + $status = $this->handleCreateCollection($version); + break; + case 'DeleteCollection': + $status = $this->handleDeleteCollection($version); + break; + case 'MoveCollection': + $status = $this->handleMoveCollection($version); + break; + case 'FolderCreate': + $status = $this->handleFolderCreate($version); + break; + case 'FolderDelete': + $status = $this->handleFolderDelete($version); + break; + case 'FolderUpdate': + $status = $this->handleFolderUpdate($version); + break; + case 'MoveItems': + $status = $this->handleMoveItems($version); + break; + case 'GetItemEstimate': + $status = $this->handleGetItemEstimate($version, $devId); + break; + case 'MeetingResponse': + $status = $this->handleMeetingResponse($version); + break; + case 'Notify': // Used for sms-based notifications (pushmail) + $status = $this->handleNotify($version); + break; + case 'Search': + $status = $this->handleSearch($devId, $version); + break; + + default: + $this->_logger->err('Unknown command - not implemented'); + $status = false; + break; + } + + return $status; + } + + /** + * Read input from the php input stream + * + * @TODO: Get rid of this - the wbxml classes have a php:// stream already + * and when we need *just* the stream and not wbxml, we can use + * $request->body + * + * @return string + */ + public function readStream() + { + $s = ""; + while (1) { + $data = fread($this->_inputStream, 4096); + if (strlen($data) == 0) { + break; + } + $s .= $data; + } + + return $s; + } + + /** + * Send the MS_Server-ActiveSync header + * + * @return void + */ + static public function activeSyncHeader() + { + header("MS-Server-ActiveSync: 6.5.7638.1"); + } + + /** + * Send the protocol versions header + * + * @return void + */ + static public function versionHeader() + { + header("MS-ASProtocolVersions: 1.0,2.0,2.1,2.5"); + } + + /** + * send protocol commands header + * + * @return void + */ + static public function commandsHeader() + { + header("MS-ASProtocolCommands: Sync,SendMail,SmartForward,SmartReply,GetAttachment,GetHierarchy,CreateCollection,DeleteCollection,MoveCollection,FolderSync,FolderCreate,FolderDelete,FolderUpdate,MoveItems,GetItemEstimate,MeetingResponse,ResolveRecipients,ValidateCert,Provision,Search,Ping"); + } + + /** + * Send provision header + * + * @return void + */ + static public function provisionHeader() + { + header("HTTP/1.1 449 Retry after sending a PROVISION command"); + } + + /** + * Obtain the policy key header from the request. + * + * @return int The policy key or zero if not set. + */ + public function getPolicyKey() + { + if (isset($this->_policykey)) { + return $this->_policykey; + } + + /* Policy key headers may be sent in either of these forms: */ + $this->_policykey = $this->_request->getHeader('X-Ms-Policykey'); + if (empty($this->_policykey)) { + $this->_policykey = $this->_request->getHeader('X-MS-PolicyKey'); + } + if (empty($this->_policykey)) { + $this->_policykey = 0; + } + + return $this->_policykey; + } + + /** + * Obtain the ActiveSync protocol version + */ + public function getProtocolVersion() + { + if (isset($this->_version)) { + return $this->_version; + } + + $this->_version = $this->_request->getHeader('Ms-Asprotocolversion'); + if (empty($this->_version)) { + $this->_version = $this->_request->getHeader('MS-ASProtocolVersion'); + } + if (empty($this->_version)) { + $this->_version = '1.0'; + } + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/ActiveSync/Factory.php b/framework/ActiveSync/lib/Horde/ActiveSync/ActiveSync/Factory.php new file mode 100644 index 000000000..9f44b08de --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/ActiveSync/Factory.php @@ -0,0 +1,33 @@ + $registry)); + $driver = new Horde_ActiveSync_Driver_Horde(array('connectory' => $connector)); + $state = new Horde_ActiveSync_StateMachine_File(array('stateDir' => '/tmp')); + $encoder = new Horde_ActiveSync_Wbxml_Encoder(fopen("php://output", "w+"), + Horde_ActiveSync::$zpushdtd); + $decoder = new Horde_ActiveSync_Wbxml_Decoder(fopen('php://input', 'r'), + Horde_ActiveSync::$zpushdtd); + + $request = new Horde_Controller_Request_Http(); + $server = new Horde_ActiveSync($driver, $state, $decoder, $encoder, $request); + } + +} diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/ContentsCache.php b/framework/ActiveSync/lib/Horde/ActiveSync/ContentsCache.php new file mode 100644 index 000000000..863fab9da --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/ContentsCache.php @@ -0,0 +1,16 @@ + + * @package Horde_ActiveSync + */ +/** + * File : diffbackend.php + * Project : Z-Push + * Descr : We do a standard differential + * change detection by sorting both + * lists of items by their unique id, + * and then traversing both arrays + * of items at once. Changes can be + * detected by comparing items at + * the same position in both arrays. + * + * Created : 01.10.2007 + * + * Zarafa Deutschland GmbH, www.zarafaserver.de + * This file is distributed under GPL v2. + * Consult LICENSE file for details + */ +abstract class Horde_ActiveSync_Driver_Base +{ + /** + * The username to sync with the backend as + * + * @var string + */ + protected $_user; + + /** + * Authenticating user + * + * @var string + */ + protected $_authUser; + + /** + * User password + * + * @var string + */ + protected $_authPass; + + /** + * Logger instance + * + * @var Horde_Log_Logger + */ + protected $_logger; + + /** + * Parameters + * + * @var array + */ + protected $_params; + + /** + * The state object for this request. Needs to be injected into this class. + * Different Sync objects may require more then one type of stateObject. + * For instance, Horde can sync contacts and caledar data with a history + * based state engine, but cannot due the same for email. + * + * @var Horde_ActiveSync_State_Base + */ + protected $_stateObject; + + /** + * Const'r + * + * @param array $params Any configuration parameters or injected objects + * the concrete driver may need. + *
+     *     (optional) logger       Horde_Log_Logger instance
+     *     (required) state_basic  A Horde_ActiveSync_State_Base object that is
+     *                             capable of handling all collections except
+     *                             email.
+     *     (optional) state_email  A Horde_ActiveSync_State_Base object that is
+     *                             capable of handling email collections.
+     *  
+ * + * @return Horde_ActiveSync_Driver + */ + public function __construct($params = array()) + { + $this->_params = $params; + + if (empty($params['state_basic']) || + !($params['state_basic'] instanceof Horde_ActiveSync_State_Base)) { + + throw new Horde_ActiveSync_Exception('Missing required state object'); + } + + // Create a stub if we don't have a useable logger. + if (isset($params['logger']) + && is_callable(array($params['logger'], 'log'))) { + $this->_logger = $params['logger']; + unset($params['logger']); + } else { + $this->_logger = new Horde_Support_Stub; + } + + $this->_stateObject = $params['state_basic']; + $this->_stateObject->setLogger($this->_logger); + $this->_stateObject->setBackend($this); + } + + /** + * Setter for the logger instance + * + * @param Horde_Log_Logger $logger The logger + * + * @void + */ + public function setLogger(Horde_Log_Logger $logger) + { + $this->_logger = $logger; + } + + /** + * Get folder stat + * "id" => The server ID that will be used to identify the folder. + * It must be unique, and not too long. How long exactly is not + * known, but try keeping it under 20 chars or so. + * It must be a string. + * "parent" => The server ID of the parent of the folder. Same restrictions + * as 'id' apply. + * "mod" => This is the modification signature. It is any arbitrary string + * which is constant as long as the folder has not changed. In + * practice this means that 'mod' can be equal to the folder name + * as this is the only thing that ever changes in folders. + */ + abstract public function statFolder($id); + + /** + * Get a folder from the backend + * + * To be implemented by concrete backend driver. + */ + abstract public function getFolder($id); + + /** + * Get the list of folders from the backend. + */ + abstract public function getFolderList(); + + /** + * Get a full list of messages on the server + * + * @param string $folderId The folder id + * @param timestamp $cutOffDate The timestamp of the earliest date for + * calendar or mail entries + * + * @return array A list of messages + */ + abstract public function GetMessageList($folderId, $cutOffDate); + + /** + * Get a list of server changes that occured during the specified time + * period. + * + * @param string $folderId The server id of the collection to check. + * @param timestamp $from_ts The starting timestamp + * @param timestamp $to_ts The ending timestamp + * + * @return array A list of messge uids that have chnaged in the specified + * time period. + */ + abstract public function getServerChanges($folderId, $from_ts, $to_ts); + + /** + * Get a message stat. + * + * @param string $folderId The folder id + * @param string $id The message id (??) + * + * @return hash with 'id', 'mod', and 'flags' members + */ + abstract public function StatMessage($folderId, $id); + + /** + * + * @param $folderid + * @param $id + * @param $truncsize + * @param $mimesupport + * + * @return Horde_ActiveSync_Message_Base The message data + */ + abstract public function GetMessage($folderid, $id, $truncsize, $mimesupport = 0); + + /** + * Delete a message + * + * @param string $folderId Folder id + * @param string $id Message id + * + * @return boolean + */ + abstract public function DeleteMessage($folderid, $id); + + /** + * Change (i.e. add or edit) a message on the backend + * + * @param string $folderId Folderid + * @param string $id Message id (maybe reorder parameteres since this may be null) + * @param Horde_ActiveSync_Message_Base $message + * + * @return a stat array of the new message + */ + abstract public function ChangeMessage($folderid, $id, $message); + + /** + * Any code needed to authenticate to backend as the actual user. + * + * @param string $username The username to authenticate as + * @param string $password The password + * @param string $domain The user domain + * + * @return boolean + */ + public function logon($username, $password, $domain = null) + { + $this->_authUser = $username; + $this->_authPass = $password; + + return true; + } + + /** + * Any code to run on log off + * + * @return boolean + */ + public function Logoff() + { + return true; + } + + /** + * Setup sync parameters. The user provided here is the user the backend + * will sync with. This allows you to authenticate as one user, and sync as + * another, if the backend supports this. + * + * @param string $user The username to sync as on the backend. + * + * @return boolean + */ + public function setup($user) + { + $this->_user = $user; + + return true; + } + + /** + * Return the helper for importing hierarchy changes from the PIM. + * + * @TODO: Probably not functional, as methods were missing from original + * codebase. + * + * @return Horde_ActiveSync_DiffState_ImportHierarchy + */ + public function GetHierarchyImporter() + { + $importer = new Horde_ActiveSync_DiffState_ImportHierarchy($this); + $importer->setLogger($this->_logger); + + return $importer; + } + + /** + * Return the helper for importing message changes from the PIM. + * + * @param string $folderid + * + * @return Horde_ActiveSync_DiffState_ImportContents + */ + public function GetContentsImporter($folderId) + { + $importer = new Horde_ActiveSync_DiffState_ImportContents($this, $folderId); + $importer->setLogger($this->_logger); + + return $importer; + } + + /** + * @TODO: This will replace the above two methods + * @return Horde_ActiveSync_Importer + */ + public function getImporter() + { + $importer = new Horde_ActiveSync_Importer($this); + //$importer->setLogger($this->_logger); + return $importer; + } + + /** + * Return helper for performing the actual sync operation. + * + * @param string $folderId + * @return unknown_type + * + */ + public function getExporter() + { + $exporter = new Horde_ActiveSync_Exporter($this); + $exporter->setLogger($this->_logger); + + return $exporter; + } + + /** + * Will (eventually) return an appropriate state object based on the class + * being sync'd. + * @param $collection + */ + public function &getStateObject($collection = array()) + { + $this->_stateObject->init($collection); + $this->_stateObject->setLogger($this->_logger); + return $this->_stateObject; + } + + /** + * Get the full folder hierarchy from the backend. + * + * @return array + */ + public function GetHierarchy() + { + $folders = array(); + + $fl = $this->getFolderList(); + foreach ($fl as $f) { + $folders[] = $this->getFolder($f['id']); + } + + return $folders; + } + + /** + * Obtain a message from the backend. + * + * @TODO: Not sure why we have this *and* GetMessage()?? + * + * @param string $folderid + * @param string $id + * @param ?? $mimesupport (Not sure what this was supposed to do) + * + * @return Horde_ActiveSync_Message_Base The message data + */ + public function Fetch($folderid, $id, $mimesupport = 0) + { + // Forces entire message (up to 1Mb) + return $this->GetMessage($folderid, $id, 1024 * 1024, $mimesupport); + } + + /** + * + * @param $attname + * @return unknown_type + */ + public function GetAttachmentData($attname) + { + return false; + } + + /** + * @param $rfc822 + * @param $forward + * @param $reply + * @param $parent + * @return unknown_type + */ + public function SendMail($rfc822, $forward = false, $reply = false, $parent = false) + { + return true; + } + + /** + * @return unknown_type + */ + public function GetWasteBasket() + { + return false; + } + + /** + * @TODO: Missing method from Z-Push + * + * @param $parent + * @param $id + * @return unknown_type + */ + public function DeleteFolder($parent, $id) + { + throw new Horde_ActiveSync_Exception('DeleteFolder not yet implemented'); + } + + /** + * + * @param $folderid + * @param $id + * @param $flags + * @return unknown_type + */ + function SetReadFlag($folderid, $id, $flags) + { + return false; + } + + /** + * @TODO: This method was missing from Z-Push + * + * @param unknown_type $parent + * @param unknown_type $id + * @param unknown_type $displayname + * @param unknown_type $type + * @return unknown_type + */ + public function changeFolder($parent, $id, $displayname, $type) + { + throw new Horde_ActiveSync_Exception('changeFolder not yet implemented.'); + } + + /** + * @todo + * + * @param $folderid + * @param $id + * @param $newfolderid + * @return unknown_type + */ + public function MoveMessage($folderid, $id, $newfolderid) + { + throw new Horde_ActiveSync_Exception('moveMessage not yet implemented.'); + } + + /** + * @todo + * + * @param $requestid + * @param $folderid + * @param $error + * @param $calendarid + * @return unknown_type + */ + public function MeetingResponse($requestid, $folderid, $error, &$calendarid) + { + throw new Horde_ActiveSync_Exception('meetingResponse not yet implemented.'); + } + + /** + * Returns array of items which contain contact information + * + * @param string $searchquery + * + * @return array + */ + public function getSearchResults($searchquery) + { + throw new Horde_ActiveSync_Exception('getSearchResults not implemented.'); + } + + /** + * Checks if the sent policykey matches the latest policykey on the server + * TODO: Revisit this once we have refactored state storage + * @param string $policykey + * @param string $devid + * + * @return status flag + */ + public function CheckPolicy($policyKey, $devId) + { + $status = SYNC_PROVISION_STATUS_SUCCESS; +// $user_policykey = $this->getPolicyKey($this->_authUser, $this->_authPass, $devId); +// if ($user_policykey != $policyKey) { +// $status = SYNC_PROVISION_STATUS_POLKEYMISM; +// } +// + return $status; + } + + /** + * Return a policy key for given user with a given device id. + * If there is no combination user-deviceid available, a new key + * should be generated. + * + * @param string $user + * @param string $pass + * @param string $devid + * + * @return unknown + */ + public function getPolicyKey($user, $pass, $devid) + { + return false; + } + + /** + * Generate a random policy key. Right now it's a 10-digit number. + * + * @return unknown + */ + public function generatePolicyKey() + { + return mt_rand(1000000000, 9999999999); + } + + /** + * Set a new policy key for the given device id. + * + * @param string $policykey + * @param string $devid + * @return unknown + */ + public function setPolicyKey($policykey, $devid) + { + return false; + } + + /** + * Return a device wipe status + * + * @param string $user + * @param string $pass + * @param string $devid + * @return int + */ + public function getDeviceRWStatus($devid) + { + return false; + } + + /** + * Set a new rw status for the device + * + * @param string $user + * @param string $pass + * @param string $devid + * @param string $status + * + * @return boolean + */ + public function setDeviceRWStatus($devid, $status) + { + return false; + } + + /** + * + * @return unknown_type + */ + public function AlterPing() + { + return false; + } + + public function AlterPingChanges($folderid, &$syncstate) + { + return array(); + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde.php b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde.php new file mode 100644 index 000000000..199804a54 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde.php @@ -0,0 +1,725 @@ + + * @package Horde_ActiveSync + */ +/*********************************************** +* File : horde.php +* Project : Z-Push +* Descr : Horde backend +* Created : 09.03.2009 +* +* � Holger de Carne holger@carne.de +* This file is distributed under GPL v2. +* Consult LICENSE file for details +************************************************/ +class Horde_ActiveSync_Driver_Horde extends Horde_ActiveSync_Driver_Base +{ + + /** Constants **/ + const APPOINTMENTS_FOLDER = 'Calendar'; + const CONTACTS_FOLDER = 'Contacts'; + const TASKS_FOLDER = 'Tasks'; + + /** + * Used for profiling + * + * @var timestamp + */ + private $_starttime; + + /** + * Cache message stats + * + * @var Array of stat hashes + */ + private $_modCache; + + /** + * Horde connector instance + * + * @var Horde_ActiveSync_Driver_Horde_Connector_Registry + */ + private $_connector; + + /** + * Const'r + * + * @param array $params Configuration parameters. + * + * @return Horde_ActiveSync_Driver_Horde + */ + public function __construct($params = array()) + { + parent::__construct($params); + if (empty($this->_params['connector'])) { + throw new Horde_ActiveSync_Exception('Missing required connector object.'); + } + $this->_connector = $params['connector']; + } + + /** + * Authenticate to Horde + * + * @TODO: Need to inject the auth handler (waiting for rpc.php refactor) + * + * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#Logon($username, $domain, $password) + */ + public function logon($username, $password, $domain = null) + { + $this->_logger->debug('Horde_ActiveSync_Driver_Horde::logon'); + parent::logon($username, $password, $domain); + + $this->_startProfiling(); + $auth = Horde_Auth::singleton($GLOBALS['conf']['auth']['driver']); + + return $auth->authenticate($username, array('password' => $password)); + } + + /** + * Clean up + * + * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#Logoff() + */ + public function Logoff() + { + $this->_logger->debug('Horde_ActiveSync_Driver_Horde::logoff'); + $this->_endProfiling(); + return true; + } + + /** + * Setup sync parameters. The user provided here is the user the backend + * will sync with. This allows you to authenticate as one user, and sync as + * another, if the backend supports this. + * + * @param string $user The username to sync as on the backend. + * + * @return boolean + */ + public function setup($user) + { + $this->_logger->debug('Horde::Setup(' . $user . ')'); + parent::setup($user); + $this->_modCache = array(); + return true; + } + + /** + * @todo + * + * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#SendMail($rfc822, $forward, $reply, $parent) + */ + public function SendMail($rfc822, $forward = false, $reply = false, $parent = false) + { + $this->_logger->debug('Horde::SendMail(...)'); + + return false; + } + + /** + * @TODO + * + * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#GetWasteBasket() + */ + public function GetWasteBasket() + { + $this->_logger->debug('Horde::GetWasteBasket()'); + + return false; + } + + /** + * Return a list of available folders + * + * @return array An array of folder stats + */ + public function getFolderList() + { + $this->_logger->debug('Horde::getFolderList()'); + $folders = array(); + + // @TODO: Be able to configure the folders to sync/not sync + $folders[] = $this->StatFolder(self::APPOINTMENTS_FOLDER); + $folders[] = $this->StatFolder(self::CONTACTS_FOLDER); + $folders[] = $this->StatFolder(self::TASKS_FOLDER); + + return $folders; + } + + /** + * Retrieve folder + * + * @param string $id The folder id + * + * @return Horde_ActiveSync_Message_Folder + */ + public function getFolder($id) + { + $this->_logger->debug('Horde::getFolder(' . $id . ')'); + + $folder = new Horde_ActiveSync_Message_Folder(); + $folder->serverid = $id; + $folder->parentid = "0"; + $folder->displayname = $id; + + switch ($id) { + case self::APPOINTMENTS_FOLDER: + $folder->type = SYNC_FOLDER_TYPE_APPOINTMENT; + break; + case self::CONTACTS_FOLDER: + $folder->type = SYNC_FOLDER_TYPE_CONTACT; + break; + case self::TASKS_FOLDER: + $folder->type = SYNC_FOLDER_TYPE_TASK; + break; + default: + return false; + } + + return $folder; + } + + /** + * Stat folder + * + * @param $id + * + * @return a stat hash + */ + public function StatFolder($id) + { + $this->_logger->debug('Horde::StatFolder(' . $id . ')'); + + $folder = array(); + $folder['id'] = $id; + $folder['mod'] = $id; + $folder['parent'] = 0; + + return $folder; + } + + /** + * Get the message list of specified folder + * + * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#GetMessageList($folderId, $cutOffDate) + */ + public function GetMessageList($folderid, $cutoffdate) + { + $this->_logger->debug('Horde::GetMessageList(' . $folderid . ', ' . $cutoffdate . ')'); + + $messages = array(); + switch ($folderid) { + case self::APPOINTMENTS_FOLDER: + $startstamp = (int)$cutoffdate; + $endstamp = time() + 32140800; //60 * 60 * 24 * 31 * 12 == one year + + try { + $events = $this->_connector->calendar_listEvents($startstamp, $endstamp, null); + } catch (Horde_Exception $e) { + $this->_logger->err($e->GetMessage()); + + return false; + } + foreach ($events as $day) { + foreach($day as $e) { + $messages[] = $this->_smartStatMessage($folderid, $e->uid, false); + } + } + break; + + case self::CONTACTS_FOLDER: + try { + $contacts = $this->_connector->contacts_list(); + } catch (Horde_Exception $e) { + $this->_logger->err($e->GetMessage()); + + return false; + } + + foreach ($contacts as $contact) { + $messages[] = $this->_smartStatMessage($folderid, $contact, true); + } + break; + + case self::TASKS_FOLDER: + // @TODO? + default: + return false; + } + + return $messages; + } + + /** + * Get a list of server changes that occured during the specified time + * period. + * + * @param string $folderId The server id of the collection to check. + * @param timestamp $from_ts The starting timestamp + * @param timestamp $to_ts The ending timestamp + * + * @return array A list of messge uids that have chnaged in the specified + * time period. + */ + public function getServerChanges($folderId, $from_ts, $to_ts) + { + $adds = $this->_connector->calendar_listBy('add', $from_ts); + $changes = $this->_connector->calendar_listBy('modify', $from_ts); + $deletes = $this->_connector->calendar_listBy('delete', $from_ts); + + // FIXME: Need to filter the results by $from_ts OR need to fix + // Horde_History to query for a timerange instead of a single timestamp + + return $changes; + } + + /** + * Get a message from the backend + * + * @TODO: Default value for truncsize? Do we need it? + * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#GetMessage($folderid, $id, $truncsize, $mimesupport) + */ + public function GetMessage($folderid, $id, $truncsize, $mimesupport = 0) + { + $this->_logger->debug('Horde::GetMessage(' . $folderid . ', ' . $id . ')'); + + $message = false; + switch ($folderid) { + case self::APPOINTMENTS_FOLDER: + try { + return $this->_connector->calendar_export($id); + } catch (Horde_Exception $e) { + $this->_logger->err($e->GetMessage()); + return false; + } + //$message = self::_fromVCalendar($event); + + break; + + case self::CONTACTS_FOLDER: + try { + $contact = $this->_connector->contacts_export($id, 'array'); + } catch (Horde_Exception $e) { + $this->_logger->err($e->GetMessage()); + return false; + } + + $message = self::_fromHash($contact); + break; + + case self::TASKS_FOLDER: + // @TODO + break; + default: + return false; + } + + return $message; + } + + /** + * Get message stat data + * + * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#StatMessage($folderId, $id) + */ + public function StatMessage($folderid, $id) + { + $this->_logger->debug('Horde::StatMessage(' . $folderid . ', ' . $id . ')'); + return $this->_smartStatMessage($folderid, $id, true); + } + + /** + * Delete a message + * + * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#DeleteMessage($folderid, $id) + */ + public function DeleteMessage($folderid, $id) + { + $this->_logger->debug('Horde::DeleteMessage(' . $folderid . ', ' . $id . ')'); + + $status = false; + switch ($folderid) { + case self::APPOINTMENTS_FOLDER: + try { + $status = $this->_connector->calendar_delete($id); + } catch (Horde_Exception $e) { + $this->_logger->err($e->getMessage()); + return false; + } + break; + + case self::CONTACTS_FOLDER: + try { + $status = $this->_connector->contacts_delete($id); + } catch (Horde_Exception $e) { + $this->_logger->err($e->getMessage()); + return false; + } + break; + + case self::TASKS_FOLDER: + break; + default: + return false; + } + + return $status; + } + + /** + * Add/Edit a message + * + * @param string $folderid + * @param string $id + * @param Horde_ActiveSync_Message_Base $message + * + * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#ChangeMessage($folderid, $id, $message) + */ + public function ChangeMessage($folderid, $id, $message) + { + $this->_logger->debug('Horde::ChangeMessage(' . $folderid . ', ' . $id . ')'); + + $stat = false; + switch ($folderid) { + case self::APPOINTMENTS_FOLDER: + if (!$id) { + try { + $id = $this->_connector->calendar_import($message); + } catch (Horde_Exception $e) { + $this->_logger->err($e->getMessage()); + return false; + } + $stat = $this->_smartStatMessage($folderid, $id, false); + } else { + // ActiveSync messages do NOT contain the serverUID value, put + // it in ourselves so we can have it during import/change. + $message->setServerUID($id); + try { + $this->_connector->calendar_replace($id, $message); + } catch (Horde_Exception $e) { + $this->_logger->err($e->getMessage()); + return false; + } + $stat = $this->_smartStatMessage($folderid, $id, false); + } + break; + + case self::CONTACTS_FOLDER: + $content = self::_toHash($message); + if (!$id) { + try { + $id = $this->_connector->contacts_import($content, 'array'); + } catch (Horde_Exception $e) { + $this->_logger->err($e->getMessage()); + return false; + } + $stat = $this->_smartStatMessage($folderid, $id, false); + } else { + try { + $this->_connector->contacts_replace($id, $content, 'array'); + } catch (Horde_Exception $e) { + $this->_logger->err($e->getMessage()); + return false; + } + $stat = $this->_smartStatMessage($folderid, $id, false); + } + case self::TASKS_FOLDER: + break; + default: + return false; + } + return $stat; + } + + /** + * + * @param string $folderid The folder id + * @param string $id The message id + * @param mixed $hint ?? + * + * @return message stat hash + */ + private function _smartStatMessage($folderid, $id, $hint) + { + $this->_logger->debug('ActiveSync_Driver_Horde::_smartStatMessage:' . $folderid . ':' . $id); + $statKey = $folderid . $id; + $mod = false; + if ($hint !== false && isset($this->_modCache[$statKey])) { + $mod = $this->_modCache[$statKey]; + } elseif (is_int($hint)) { + $mod = $hint; + $this->_modCache[$statKey] = $mod; + } else { + switch ($folderid) { + case self::APPOINTMENTS_FOLDER: + $mod = $this->_connector->calendar_getActionTimestamp($id, 'modify'); + break; + case self::CONTACTS_FOLDER: + $mod = $this->_connector->contacts_getActionTimestamp($id, 'modify'); + break; + case self::TASKS_FOLDER: + break; + default: + return false; + } + $this->_modCache[$statKey] = $mod; + } + $message = array(); + $message['id'] = $id; + $message['mod'] = $mod; + $message['flags'] = 1; + + return $message; + } + + /** + * Start profiling data + * + * @return void + */ + private function _startProfiling() + { + $this->_starttime = microtime(true); + } + + /** + * End profiling + * + * @return void + */ + private function _endProfiling() + { + $this->_logger->debug('Session lasted ' . sprintf('%0.3f',microtime(true) - $this->_starttime) . ' s'); + } + + + /** + * Create a hash suitable for importing into contacts/import or + * contacts/replace from a Horde_ActiveSync_Message_Base object. + * + * @param Horde_ActiveSync_Message_Contact $message + * + * @return array + */ + private static function _toHash(Horde_ActiveSync_Message_Contact $message) + { + $charset = Horde_Nls::getCharset(); + $formattedname = false; + + /* Name */ + $hash['name'] = Horde_String::convertCharset($message->fileas, 'utf-8', $charset); + $hash['lastname'] = Horde_String::convertCharset($message->lastname, 'utf-8', $charset); + $hash['firstname'] = Horde_String::convertCharset($message->firstname, 'utf-8', $charset); + $hash['middlenames'] = Horde_String::convertCharset($message->middlename, 'utf-8', $charset); + $hash['namePrefix'] = Horde_String::convertCharset($message->title, 'utf-8', $charset); + $hash['nameSuffix'] = Horde_String::convertCharset($message->suffix, 'utf-8', $charset); + + + // picture ($message->picture *should* already be base64 encdoed) + $hash['photo'] = base64_decode($message->picture); + + /* Home */ + $hash['homeStreet'] = Horde_String::convertCharset($message->homestreet, 'utf-8', $charset); + $hash['homeCity'] = Horde_String::convertCharset($message->homecity, 'utf-8', $charset); + $hash['homeProvince'] = Horde_String::convertCharset($message->homestate, 'utf-8', $charset); + $hash['homePostalCode'] = $message->homepostalcode; + $hash['homeCountry'] = Horde_String::convertCharset($message->homecountry, 'utf-8', $charset); + + /* Business */ + $hash['workStreet'] = Horde_String::convertCharset($message->businessstreet, 'utf-8', $charset); + $hash['workCity'] = Horde_String::convertCharset($message->businesscity, 'utf-8', $charset); + $hash['workProvince'] = Horde_String::convertCharset($message->businessstate, 'utf-8', $charset); + $hash['workPostalCode'] = $message->businesspostalcode; + $hash['workCountry'] = Horde_String::convertCharset($message->businesscountry, 'utf-8', $charset); + + $hash['homePhone'] = $message->homephonenumber; + $hash['workPhone'] = $message->businessphonenumber; + $hash['fax'] = $message->businessfaxnumber; + $hash['pager'] = $message->pagernumber; + $hash['cellPhone'] = $message->mobilephonenumber; + + /* Email addresses */ + $hash['email'] = Horde_iCalendar_vcard::getBareEmail($message->email1address); + + /* Job title */ + $hash['title'] = Horde_String::convertCharset($message->jobtitle, 'utf-8', $charset); + + $hash['company'] = Horde_String::convertCharset($message->companyname, 'utf-8', $charset); + $hash['department'] = Horde_String::convertCharset($message->department, 'utf-8', $charset); + + /* Categories */ + $hash['category']['value'] = Horde_String::convertCharset(implode(';', $message->categories), 'utf-8', $charset); + $hash['category']['new'] = true; + /* Children */ + // @TODO + + /* Spouse */ + $hash['spouse'] = Horde_String::convertCharset($message->spouse, 'utf-8', $charset); + + /* Notes */ + $hash['notes'] = Horde_String::convertCharset($message->body, 'utf-8', $charset); + + /* webpage */ + $hash['website'] = Horde_String::convertCharset($message->webpage, 'utf-8', $charset); + + /* Birthday and Anniversary */ + if (!empty($message->birthday)) { + $bday = new Horde_Date($message->birthday); + $hash['birthday'] = $bday->format('Y-m-d'); + } else { + $hash['birthday'] = null; + } + if (!empty($message->anniversary)) { + $anniversary = new Horde_Date($message->anniversary); + $hash['anniversary'] = $anniversary->format('Y-m-d'); + } else { + $hash['anniversary'] = null; + } + + /* Assistant */ + $hash['assistant'] = Horde_String::convertCharset($message->assistantname, 'utf-8', $charset); + + return $hash; + } + + /** + * Import data from Horde's contacts API + * + * @param array $hash A hash as returned from contacts/export + * + * @return Horde_ActiveSync_Message_Base object + */ + private static function _fromHash($hash) + { + $message = new Horde_ActiveSync_Message_Contact(); + + $charset = Horde_Nls::getCharset(); + + foreach ($hash as $field => $value) { + switch ($field) { + case 'name': + $message->fileas = Horde_String::convertCharset($value, $charset, 'utf-8'); + break; + case 'lastname': + $message->lastname = Horde_String::convertCharset($value, $charset, 'utf-8'); + break; + case 'firstname': + $message->firstname = Horde_String::convertCharset($value, $charset, 'utf-8'); + break; + case 'middlenames': + $message->middlename = Horde_String::convertCharset($value, $charset, 'utf-8'); + break; + case 'namePrefix': + $message->title = Horde_String::convertCharset($value, $charset, 'utf-8'); + break; + case 'nameSuffix': + $message->suffix = Horde_String::convertCharset($value, $charset, 'utf-8'); + break; + + case 'photo': + $message->picture = base64_encode($value['load']['data']); + break; + + /* Address (TODO: check for a single home/workAddress field instead) */ + case 'homeStreet': + $message->homestreet = Horde_String::convertCharset($hash['homeStreet'], $charset, 'utf-8'); + break; + case 'homeCity': + $message->homecity = Horde_String::convertCharset($hash['homeCity'], $charset, 'utf-8'); + break; + case 'homeProvince': + $message->homestate = Horde_String::convertCharset($hash['homeProvince'], $charset, 'utf-8'); + break; + case 'homePostalCode': + $message->homepostalcode = Horde_String::convertCharset($hash['homePostalCode'], $charset, 'utf-8'); + break; + case 'homeCountry': + $message->homecountry = Horde_String::convertCharset($hash['homeCountry'], $charset, 'utf-8'); + break; + case 'workStreet': + $message->businessstreet = Horde_String::convertCharset($hash['workStreet'], $charset, 'utf-8'); + break; + case 'workCity': + $message->businesscity = Horde_String::convertCharset($hash['workCity'], $charset, 'utf-8'); + break; + case 'workProvince': + $message->businessstate = Horde_String::convertCharset($hash['workProvince'], $charset, 'utf-8'); + break; + case 'workPostalCode': + $message->businesspostalcode = Horde_String::convertCharset($hash['workPostalCode'], $charset, 'utf-8'); + break; + case 'workCountry': + $message->businesscountry = Horde_String::convertCharset($hash['workCountry'], $charset, 'utf-8'); + break; + case 'homePhone': + /* Phone */ + $message->homephonenumber = $hash['homePhone']; + break; + case 'cellPhone': + $message->mobilephonenumber = $hash['cellPhone']; + break; + case 'fax': + $message->businessfaxnumber = $hash['fax']; + break; + case 'workPhone': + $message->businessphonenumber = $hash['workPhone']; + break; + case 'pager': + $message->pagernumber = $hash['pager']; + break; + + case 'email': + $message->email1address = Horde_iCalendar_vcard::getBareEmail($value); + break; + + case 'title': + $message->jobtitle = Horde_String::convertCharset($value, $charset, 'utf-8'); + break; + + case 'company': + $message->companyname = Horde_String::convertCharset($value, $charset, 'utf-8'); + break; + case 'departnemt': + $message->department = Horde_String::convertCharset($value, $charset, 'utf-8'); + break; + + case 'category': + // Categories FROM horde are a simple string value, going BACK to horde are an array with 'value' and 'new' keys + $message->categories = explode(';', Horde_String::convertCharset($value, $charset, 'utf-8')); + break; + + case 'spouse': + $message->spouse = Horde_String::convertCharset($value, $charset, 'utf-8'); + break; + case 'notes': + $message->body = Horde_String::convertCharset($value, $charset, 'utf-8'); + $message->bodysize = strlen($message->body); + $message->bodytruncated = false; + break; + case 'website': + $message->webpage = $value; + break; + + case 'birthday': + case 'anniversary': + if (!empty($value)) { + $date = new Horde_Date($value); + $message->{$field} = $date; + } else { + $message->$field = null; + } + break; + } + } + + return $message; + } + +} diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde/Connector/Registry.php b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde/Connector/Registry.php new file mode 100644 index 000000000..86edbaa8e --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde/Connector/Registry.php @@ -0,0 +1,217 @@ +_registry = $params['registry']; + } + + /** + * Get a list of events from horde's calendar api + * + * @param timestamp $startstamp The start of time period. + * @param timestamp $endstamp The end of time period + * @param string $calendar The calendar(s) to get events for + * + * @return array + */ + public function calendar_listEvents($startstamp, $endstamp, $calendar) + { + $result = $this->_registry->calendar->listEvents( + $startstamp, // Start + $endstamp, // End + $calendar, // Calendar + false, // Recurrence + false, // Alarms only + false, // Show remote + true); // Hide exception events + + return $result; + } + + /** + * Get a list of event uids that have had $action happen since $from_ts. + * Optionally limits to a specific calendar. + * + * @param string $action The action to check for (add, modify, delete) + * @param timestamp $from_ts The timestamp to start checking from + * @param string $calendar + */ + public function calendar_listBy($action, $from_ts, $calendar = null) + { + return $this->_registry->calendar->listBy($action, $from_ts, $calendar); + } + + /** + * Export the specified calendar in the specified content type + * + * @param string $uid The calendar id + * @param string $contentType The content type specifier + * + * @return The iCalendar representation of the calendar + */ + public function calendar_export($uid) + { + $result = $this->_registry->calendar->export($uid, 'activesync'); + return $result; + } + + /** + * Import an event into Horde's calendar store. + * + * @param Horde_ActiveSync_Message_Appointmetn $content The event content + * @param string $contentType The content type of $content + * @param string $calendar The calendar to import event into + * + * @return string The event's UID + */ + public function calendar_import($content, $calendar = null) + { + return $this->_registry->calendar->import($content, 'activesync', $calendar); + } + + /** + * Replcae the event with new data + * + * @param string $uid The UID of the event to replace + * @param string $content The new event content + * @param string $contentType The content type of $content + * + * @return boolean + */ + public function calendar_replace($uid, $content) + { + $result = $this->_registry->calendar->replace($uid, $content, 'activesync'); + return $result; + } + + /** + * Delete an event from Horde's calendar storage + * + * @param string $uid The UID of the event to delete + * + * @return boolean + */ + public function calendar_delete($uid) + { + $result = $this->_registry->calendar->delete($uid); + return $result; + } + + /** + * Return the timestamp for the last time $action was performed. + * + * @param string $uid The UID of the event we are interested in. + * @param string $action The action we are interested in (add, modify...) + * + * @return timestamp + */ + public function calendar_getActionTimestamp($uid, $action) + { + $result = $this->_registry->calendar->getActionTimestamp($uid, $action); + return $result; + } + + /** + * Get a list of all contacts a user can see + * + * @return array of contact UIDs + */ + public function contacts_list() + { + $result = $this->_registry->contacts->listContacts(); + return $result; + } + + /** + * Export the specified contact from Horde's contacts storage + * + * @param string $uid The contact's UID + * @param string $contentType The content type to export in + * (text/directory text/vcard text/x-vcard) + * + * @return the contact in the requested content type + */ + public function contacts_export($uid, $contentType) + { + $result = $this->_registry->contacts->export($uid, $contentType); + return $result; + } + + /** + * Import the provided contact data into Horde's contacts storage + * + * @param string $content The contact data + * @param string $contentType The content type specifier of $content + * @param string $source The contact source to import to + * + * @return boolean + */ + public function contacts_import($content, $contentType, $import_source = null) + { + $result = $this->_registry->contacts->import($content, $contentType, $import_source); + return $result; + } + + /** + * Replace the specified contact with the data provided. + * + * @param string $uid The UID of the contact to replace + * @param string $content The contact data + * @param string $contentType The content type of $content + * @param string $sources The sources where UID will be replaced + * + * @return boolean + */ + public function contacts_replace($uid, $content, $contentType, $sources = null) + { + $result = $this->_registry->contacts->replace($uid, $content, $contentType, $sources); + return $result; + } + + /** + * Delete the specified contact + * + * @param string $uid The UID of the contact to remove + * + * @return bolean + */ + public function contacts_delete($uid) + { + $result = $this->_registry->contacts->delete($uid); + return $result; + } + + /** + * Get the timestamp of the most recent occurance of $action for the + * specifed contact + * + * @param string $uid The UID of the contact to search + * @param string $action The action to lookup + * + * @return timestamp + */ + public function contacts_getActionTimestamp($uid, $action) + { + $result = $this->_registry->contacts->getActionTimestamp($uid, $action); + return $result; + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Exception.php b/framework/ActiveSync/lib/Horde/ActiveSync/Exception.php new file mode 100644 index 000000000..e380d73b6 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Exception.php @@ -0,0 +1,3 @@ + + * @package Horde_ActiveSync + * + */ +/*********************************************** +* File : diffbackend.php +* Project : Z-Push +* Descr : We do a standard differential +* change detection by sorting both +* lists of items by their unique id, +* and then traversing both arrays +* of items at once. Changes can be +* detected by comparing items at +* the same position in both arrays. +* +* Created : 01.10.2007 +* +* � Zarafa Deutschland GmbH, www.zarafaserver.de +* This file is distributed under GPL v2. +* Consult LICENSE file for details +************************************************/ + +/** + * This class handles preparing the diff data for sending back to the PIM. Takes + * the data from the Importer, syncronizes it and tracks the state. + * + * @author Michael J. Rubinsky + * @package Horde_ActiveSync + */ +class Horde_ActiveSync_Exporter +{ + /** + * Local copy of changes to push to PIM + * + * @var array + */ + protected $_changes; + + /** + * Tracks the number of changes that have been sent. + * + * @var int + */ + protected $_step = 0; + + /** + * Server specific folder id + * + * @var string + */ + protected $_folderId; + + /** + * The collection type for this folder + * + * @var string + */ + protected $_collection; + + /** + * The backend driver + * + * @var Horde_ActiveSync_Driver_Base + */ + protected $_backend; + + /** + * Any flags + * ??? + * @var + */ + protected $_flags; + + /** + * The statemachine + * + * @var Horde_ActiveSynce_StateMachine_Base + */ + protected $_state; + + /** + * The current syncKey for this request + * + * @var string + */ + protected $_syncKey; + + /** + * The change streamer + * + * @var Horde_ActiveSync_Streamer + */ + protected $_streamer; + + protected $_logger; + + /** + * Const'r + * + * @param $backend + */ + public function __construct(Horde_ActiveSync_Driver_Base $backend) + { + $this->_backend = $backend; + } + + public function init(Horde_ActiveSync_State_Base &$stateMachine, + $streamer, + $collection = array()) + { + $this->_stateMachine = &$stateMachine; + $this->_streamer = $streamer; + $this->_folderId = !empty($collection['id']) ? $collection['id'] : false; + $this->_changes = $stateMachine->getChanges(); + $this->_syncKey = $collection['synckey']; + $this->_truncation = !empty($collection['truncation']) ? $collection['truncation'] : 0; + } + + public function setLogger($logger) + { + $this->_logger = $logger; + } + + /** + * Sends the next change in the set and updates the stateMachine if + * successful + * + * @return mixed A progress array or false if no more changes + */ + public function syncronize($flags = 0) + { + $progress = array(); + + if ($this->_folderId == false) { + //@TODO: Folder changes not implemented?? + if ($this->_step < count($this->_changes)) { + $change = $this->_changes[$this->_step]; + + switch($change['type']) { + case 'change': + $folder = $this->_backend->getFolder($change['id']); + $stat = $this->_backend->StatFolder($change['id']); + if (!$folder) { + return; + } + + if ($flags & BACKEND_DISCARD_DATA || $this->_streamer->FolderChange($folder)) { + $this->_stateMachine->updateState('change', $stat); + } + break; + case 'delete': + if ($flags & BACKEND_DISCARD_DATA || $this->_streamer->FolderDeletion($change['id'])) { + $this->_stateMachine->updateState('delete', $change); + } + break; + } + + $this->_step++; + + $progress = array(); + $progress['steps'] = count($this->_changes); + $progress['progress'] = $this->_step; + + return $progress; + } else { + return false; + } + } else { + if ($this->_step < count($this->_changes)) { + $change = $this->_changes[$this->_step]; + + switch($change['type']) { + case 'change': + $truncsize = self::_getTruncSize($this->_truncation); + // Note: because 'parseMessage' and 'statMessage' are two seperate + // calls, we have a chance that the message has changed between both + // calls. This may cause our algorithm to 'double see' changes. + $stat = $this->_backend->StatMessage($this->_folderId, $change['id']); + if (!$message = $this->_backend->GetMessage($this->_folderId, $change['id'], $truncsize)) { + return false; + } + + // copy the flag to the message + $message->flags = (isset($change['flags'])) ? $change['flags'] : 0; + + if ($stat && $message) { + if ($flags & BACKEND_DISCARD_DATA || $this->_streamer->messageChange($change['id'], $message) == true) { + $this->_stateMachine->updateState('change', $stat); + } + } + break; + + case 'delete': + if ($flags & BACKEND_DISCARD_DATA || $this->_streamer->messageDeletion($change['id']) == true) { + $this->_stateMachine->updateState('delete', $change); + } + break; + + case 'flags': + if ($flags & BACKEND_DISCARD_DATA || $this->_streamer->messageReadFlag($change['id'], $change['flags']) == true) { + $this->_stateMachine->updateState('flags', $change); + } + break; + + case 'move': + if ($flags & BACKEND_DISCARD_DATA || $this->_streamer->messageMove($change['id'], $change['parent']) == true) { + $this->_stateMachine->updateState('move', $change); + } + break; + } + + $this->_step++; + + $progress = array(); + $progress['steps'] = count($this->_changes); + $progress['progress'] = $this->_step; + + return $progress; + } else { + return false; + } + } + } + + public function getChangeCount() + { + return $this->_stateMachine->getChangeCount(); + } + + /** + * + * @param $truncation + * @return unknown_type + */ + private static function _getTruncSize($truncation) + { + switch($truncation) { + case SYNC_TRUNCATION_HEADERS: + return 0; + case SYNC_TRUNCATION_512B: + return 512; + case SYNC_TRUNCATION_1K: + return 1024; + case SYNC_TRUNCATION_5K: + return 5 * 1024; + case SYNC_TRUNCATION_SEVEN: + case SYNC_TRUNCATION_ALL: + return 1024 * 1024; // We'll limit to 1MB anyway + default: + return 1024; // Default to 1Kb + } + } + +} diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/HierarchyCache.php b/framework/ActiveSync/lib/Horde/ActiveSync/HierarchyCache.php new file mode 100644 index 000000000..123b543bf --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/HierarchyCache.php @@ -0,0 +1,47 @@ +changed = array(); + $this->deleted = array(); + $this->count = 0; + + return true; + } + + public function FolderChange($folder) + { + array_push($this->changed, $folder); + $this->count++; + + return true; + } + + public function FolderDeletion($id) + { + array_push($this->deleted, $id); + $this->count++; + + return true; + } +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Importer.php b/framework/ActiveSync/lib/Horde/ActiveSync/Importer.php new file mode 100644 index 000000000..913c2ce46 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Importer.php @@ -0,0 +1,261 @@ + + */ + protected $_flags; + + /** + * The server specific folder id + * + * @var string + */ + protected $_folderId; + + protected $_logger; + + /** + * Const'r + * + * @param Horde_ActiveSync_Driver_Base $backend + * @param Horde_ActiveSync_StateMachine_Base $stateMachine + * @param $syncKey + * @param $flags + */ + public function __construct(Horde_ActiveSync_Driver_Base $backend) + { + $this->_backend = $backend; + } + + public function init(Horde_ActiveSync_State_Base &$stateMachine, + $folderId, $syncKey, $flags = 0) + { + $this->_stateMachine = &$stateMachine; + $this->_syncKey = $syncKey; + $this->_flags = $flags; + $this->_folderId = $folderId; + } + + public function setLogger($logger) + { + $this->_logger = $logger; + } + + /** + * + * @param mixed $id A server message id or + * false if a new message + * @param Horde_ActiveSync_Message_Base $message A message object + * + * @return mixed The server message id or false + */ + public function ImportMessageChange($id, $message) + { + //do nothing if it is in a dummy folder + if ($this->_folderId == SYNC_FOLDER_TYPE_DUMMY) { + return false; + } + + if ($id) { + // See if there's a conflict + $conflict = $this->_isConflict('change', $this->_folderId, $id); + + // Update client state if this is an update + $change = array(); + $change['id'] = $id; + $change['mod'] = 0; // dummy, will be updated later if the change succeeds + $change['parent'] = $this->_folderId; + $change['flags'] = (isset($message->read)) ? $message->read : 0; + $this->_stateMachine->updateState('change', $change); + + if ($conflict && $this->_flags == SYNC_CONFLICT_OVERWRITE_PIM) { + return true; + } + } + + $stat = $this->_backend->ChangeMessage($this->_folderId, $id, $message); + // @TODO: Isn't this an error? + if (!is_array($stat)) { + return $stat; + } + + // Record the state of the message + $this->_stateMachine->updateState('change', $stat); + + return $stat['id']; + } + + /** + * Import a deletion. This may conflict if the local object has been + * modified. + * + * @param string $id Server message id + */ + public function ImportMessageDeletion($id) + { + //do nothing if it is in a dummy folder + if ($this->_folderId == SYNC_FOLDER_TYPE_DUMMY) { + return true; + } + + // See if there's a conflict + $conflict = $this->_isConflict('delete', $this->_folderId, $id); + + // Update client state + $change = array(); + $change['id'] = $id; + $this->_stateMachine->updateState('delete', $change); + + // If there is a conflict, and the server 'wins', then return OK without + // performing the change this will cause the exporter to 'see' the + // overriding item as a change, and send it back to the PIM + if ($conflict && $this->_flags == SYNC_CONFLICT_OVERWRITE_PIM) { + return true; + } + + $this->_backend->DeleteMessage($this->_folderId, $id); + + return true; + } + + /** + * Import a change in 'read' flags .. This can never conflict + * + * @param string $id Server message id + * @param ?? $flags The read flags to set + */ + public function ImportMessageReadFlag($id, $flags) + { + //do nothing if it is a dummy folder + if ($this->_folderId == SYNC_FOLDER_TYPE_DUMMY) { + return true; + } + + // Update client state + $change = array(); + $change['id'] = $id; + $change['flags'] = $flags; + $this->_stateMachine->updateState('flags', $change); + $this->_backend->SetReadFlag($this->_folderId, $id, $flags); + + return true; + } + + /** + * Not supported/todo? + * + * @param $id + * @param $newfolder + * @return + */ + public function ImportMessageMove($id, $newfolder) + { + return true; + } + + /** + * + * @param $id + * @param $parent + * @param $displayname + * @param $type + * @return unknown_type + */ + public function ImportFolderChange($id, $parent, $displayname, $type) + { + //do nothing if it is a dummy folder + if ($parent == SYNC_FOLDER_TYPE_DUMMY) { + return false; + } + + if ($id) { + $change = array(); + $change['id'] = $id; + $change['mod'] = $displayname; + $change['parent'] = $parent; + $change['flags'] = 0; + $this->_stateMachine->updateState('change', $change); + } + + // @TODO: ChangeFolder did not exist in ZPush's code?? + $stat = $this->_backend->ChangeFolder($parent, $id, $displayname, $type); + if ($stat) { + $this->_stateMachine->updateState('change', $stat); + } + + return $stat['id']; + } + + /** + * + * @param $id + * @param $parent + * @return unknown_type + */ + public function ImportFolderDeletion($id, $parent) + { + //do nothing if it is a dummy folder + if ($parent == SYNC_FOLDER_TYPE_DUMMY) { + return false; + } + + $change = array(); + $change['id'] = $id; + + $this->_stateMachine->updateState('delete', $change); + $this->_backend->DeleteFolder($parent, $id); + + return true; + } + + /** + * Returns TRUE if the given ID conflicts with the given operation. + * This is only true in the following situations: + * + * Changed here and changed there + * Changed here and deleted there + * Deleted here and changed there + * + * Any other combination of operations can be done + * (e.g. change flags & move or move & delete) + */ + protected function _isConflict($type, $folderid, $id) + { + $stat = $this->_backend->StatMessage($folderid, $id); + if (!$stat) { + // Message is gone + if ($type == 'change') { + return true; + } else { + return false; // all other remote changes still result in a delete (no conflict) + } + } + + return $this->_stateMachine->isConflict($stat, $type); + } +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Appointment.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Appointment.php new file mode 100644 index 000000000..6b7ed5ac4 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Appointment.php @@ -0,0 +1,722 @@ + + * @package Horde_ActiveSync + */ +class Horde_ActiveSync_Message_Appointment extends Horde_ActiveSync_Message_Base +{ + /* Sensitivity */ + const SENSITIVITY_NORMAL = 0; + const SENSITIVITY_PERSONAL = 1; + const SENSITIVITY_PRIVATE = 2; + const SENSITIVITY_CONFIDENTIAL = 3; + + /* Busy status */ + const BUSYSTATUS_FREE = 0; + const BUSYSTATUS_TENATIVE = 1; + const BUSYSTATUS_BUSY = 2; + const BUSYSTATUS_OUT = 3; + + /* All day meeting */ + const IS_ALL_DAY = 1; + + /* Meeting status */ + const MEETING_NOT_MEETING = 0; + const MEETING_IS_MEETING = 1; + const MEETING_RECEIVED = 3; + const MEETING_CANCELLED = 5; + const MEETING_CANCELLED_RECEIVED = 7; + + /* Response status */ + const RESPONSE_NONE = 0; + const RESPONSE_ORGANIZER = 1; + const RESPONSE_TENATIVE = 2; + const RESPONSE_ACCEPTED =3; + const RESPONSE_DECLINED = 4; + const RESPONSE_NORESPONSE = 5; // Not sure what difference this is to NONE? + + /** + * Workarounds for PHP < 5.2.6 not being able to return an array by reference + * from a __get() property. + */ + public $exceptions = array(); + public $attendees; + public $categories; + + /** + * Constructor + * + * @param array $params + * + * @return Horde_ActiveSync_Message_Appointment + */ + public function __construct($params = array()) { + $mapping = array( + SYNC_POOMCAL_TIMEZONE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'timezone'), + SYNC_POOMCAL_DTSTAMP => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'dtstamp', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE), + SYNC_POOMCAL_STARTTIME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'starttime', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE), + SYNC_POOMCAL_SUBJECT => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'subject'), + SYNC_POOMCAL_UID => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'uid', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_HEX), + SYNC_POOMCAL_ORGANIZERNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'organizername'), + SYNC_POOMCAL_ORGANIZEREMAIL => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'organizeremail'), + SYNC_POOMCAL_LOCATION => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'location'), + SYNC_POOMCAL_ENDTIME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'endtime', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE), + SYNC_POOMCAL_RECURRENCE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'recurrence', Horde_ActiveSync_Message_Base::KEY_TYPE => 'Horde_ActiveSync_Message_Recurrence'), + SYNC_POOMCAL_SENSITIVITY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'sensitivity'), + SYNC_POOMCAL_BUSYSTATUS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'busystatus'), + SYNC_POOMCAL_ALLDAYEVENT => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'alldayevent'), + SYNC_POOMCAL_REMINDER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'reminder'), + SYNC_POOMCAL_RTF => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'rtf'), + SYNC_POOMCAL_MEETINGSTATUS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'meetingstatus'), + SYNC_POOMCAL_ATTENDEES => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'attendees', Horde_ActiveSync_Message_Base::KEY_TYPE => 'Horde_ActiveSync_Message_Attendee', Horde_ActiveSync_Message_Base::KEY_VALUES => SYNC_POOMCAL_ATTENDEE), + SYNC_POOMCAL_BODY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'body'), + SYNC_POOMCAL_BODYTRUNCATED => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'bodytruncated'), + SYNC_POOMCAL_EXCEPTIONS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'exceptions', Horde_ActiveSync_Message_Base::KEY_TYPE => 'Horde_ActiveSync_Message_Exception', Horde_ActiveSync_Message_Base::KEY_VALUES => SYNC_POOMCAL_EXCEPTION), + SYNC_POOMCAL_CATEGORIES => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'categories', Horde_ActiveSync_Message_Base::KEY_VALUES => SYNC_POOMCAL_CATEGORY), + SYNC_POOMCAL_RESPONSETYPE => array(Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'responsetype'), + ); + + parent::__construct($mapping, $params); + } + + /** + * Set the timezone + * + * @param mixed $date Either a Horde_Date or timezone descriptor such as + * America/New_York etc... + * + */ + public function setTimezone($date) + { + if (!($date instanceof Horde_Date)) { + $timezone = new Horde_Date(time(), $date); + } + $offsets = Horde_ActiveSync_Timezone::getOffsetsFromDate($date); + $tz = Horde_ActiveSync_Timezone::getSyncTZFromOffsets($offsets); + $this->_properties['timezone'] = $tz; + } + + /** + * Set the appointment's modify timestamp + * + * @param mixed $timestamp Horde_Date or a unix timestamp + */ + public function setDTStamp($date) + { + if (!($date instanceof Horde_Date)) { + $date = new Horde_Date($date); + } + $this->_properties['dtstamp'] = $date; + } + + /** + * Get the appointment's dtimestamp + * + */ + public function getDTStamp() + { + return $this->_getAttribute('dtstamp'); + } + + /** + * Set the appointment time/duration. + * + * @param array $timestamp 'start', 'end' or 'duration' (in seconds) or 'allday' + */ + public function setDatetime($datetime = array()) + { + /* Start date is always required */ + if (empty($datetime['start'])) { + throw new InvalidArgumentException('Missing the required start parameter'); + } + + /* Get or calculate start and end time in local tz */ + $start = clone($datetime['start']); + if (!empty($datetime['end'])) { + $end = clone($datetime['end']); + } elseif (!empty($datetime['duration'])) { + $end = clone($start); + $end->sec += $datetime['duration']; + } else { + $end = clone($start); + } + + /*Is this an all day event? */ + if ($start->hour == 0 && + $start->min == 0 && + $start->sec == 0 && + $end->hour == 23 && + $end->min == 59) { + + $end = new Horde_Date( + array('year' => (int)$end->year, + 'month' => (int)$end->month, + 'mday' => (int)$end->mday + 1)); + + $this->_properties['alldayevent'] = self::IS_ALL_DAY; + } elseif (!empty($datetime['allday'])) { + $this->_properties['alldayevent'] = self::IS_ALL_DAY; + } + $this->_properties['starttime'] = $start; + $this->_properties['endtime'] = $end; + } + + /** + * Get the appointment's time data + * + * @return array containing 'start', 'end', 'allday' + */ + public function getDatetime() + { + return array( + 'start' => $this->_properties['starttime'], + 'end' => $this->_properties['endtime'], + 'allday' => !empty($this->_properties['alldayevent']) ? true : false + ); + } + + /** + * Set the appointment subject field. + * + * @param string $subject UTF-8 string + */ + public function setSubject($subject) + { + $this->_properties['subject'] = $subject; + } + + public function getSubject() + { + return $this->_getAttribute('subject'); + } + + /** + * Set the appointment uid. Note that this is the PIM's UID value, and not + * the value that the server uses for the UID. ActiveSync messages do not + * include any server uid value as part of the message natively. + * + * @param string $uid The server's uid for this appointment + */ + public function setUid($uid) + { + $this->_properties['uid'] = $uid; + } + + /** + * Get the PIM's UID. See not above regarding server UIDs. + * + * @return string + */ + public function getUid() + { + return $this->_getAttribute('uid'); + } + + /** + * Because the PIM doesn't pass the server uid as part of the message, + * we need to add it manually so the backend can have access to it + * when changing this object. + * + * @param string $uid The server UID + */ + public function setServerUID($uid) + { + $this->_getAttribute('serveruid'); + } + + /** + * Obtain the server UID. See note above. + * + * @return string + */ + public function getServerUID() + { + return $this->_getAttribute('serveruid'); + } + + /** + * Set the organizer name and/or email + * + * @param array 'name' and 'email' for this appointment organizer. + */ + public function setOrganizer($organizer) + { + $this->_properties['organizername'] = !empty($organizer['name']) + ? $organizer['name'] + : ''; + + $this->_properties['organizeremail'] = !empty($organizer['email']) + ? $organizer['email'] + : ''; + } + + /** + * Get the details for the appointment organizer + * + * @return array with 'name' and 'email' values + */ + public function getOrganizer() + { + return array('name' => $this->_getAttribute('organizername'), + 'email' => $this->_getAttribute('organizeremail')); + } + + /** + * Set appointment location field. + * + * @param string $location + */ + public function setLocation($location) + { + $this->_properties['location'] = $location; + } + + /** + * Get the location field + * + * @return string + */ + public function getLocation() + { + return $this->_getAttribute('location'); + } + + /** + * Set recurrence information for this appointment + * + * @param Horde_Date_Recurrence $recurrence + */ + public function setRecurrence(Horde_Date_Recurrence $recurrence) + { + $r = new Horde_ActiveSync_Message_Recurrence(); + + /* Map the type fields */ + switch ($recurrence->recurType) { + case Horde_Date_Recurrence::RECUR_DAILY: + $r->type = Horde_ActiveSync_Message_Recurrence::TYPE_DAILY; + break; + case Horde_Date_Recurrence::RECUR_WEEKLY; + $r->type = Horde_ActiveSync_Message_Recurrence::TYPE_WEEKLY; + $r->dayofweek = $recurrence->getRecurOnDays(); + break; + case Horde_Date_Recurrence::RECUR_MONTHLY_DATE: + $r->type = Horde_ActiveSync_Message_Recurrence::TYPE_MONTHLY; + break; + case Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY; + $r->type = Horde_ActiveSync_Message_Recurrence::TYPE_MONTHLY_NTH; + $r->dayofweek = $recurrence->getRecurOnDays(); + break; + case Horde_Date_Recurrence::RECUR_YEARLY_DATE: + $r->type = Horde_ActiveSync_Message_Recurrence::TYPE_YEARLY; + break; + case Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY: + $r->type = Horde_ActiveSync_Message_Recurrence::TYPE_YEARLYNTH; + $r->dayofweek = $recurrence->getRecurOnDays(); + break; + } + + $this->_properties['recurrence'] = $r; + } + + /** + * Obtain a recurrence object. Note this returns a Horde_Date_Recurrence + * object, not Horde_ActiveSync_Message_Recurrence. + * + * @return Horde_Date_Recurrence + */ + public function getRecurrence() + { + if (!$recurrence = $this->_getAttribute('recurrence')) { + return false; + } + + $rrule = new Horde_Date_Recurrence(new Horde_Date($this->_getAttribute('startdate'))); + + /* Map MS AS type field to Horde_Date_Recurrence types */ + switch ($recurrence->type) { + case Horde_ActiveSync_Message_Recurrence::TYPE_DAILY: + $rrule->setRecurType(Horde_Date_Recurrence::RECUR_DAILY); + break; + case Horde_ActiveSync_Message_Recurrence::TYPE_WEEKLY: + $rrule->setRecurType(Horde_Date_Recurrence::RECUR_WEEKLY); + $rrule->setRecurOnDay($recurrence->dayofweek); + break; + case Horde_ActiveSync_Message_Recurrence::TYPE_MONTHLY: + $rrule->setRecurType(Horde_Date_Recurrence::RECUR_MONTHLY_DATE); + break; + case Horde_ActiveSync_Message_Recurrence::TYPE_MONTHLY_NTH: + $rrule->setRecurType(Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY); + $rrule->setRecurOnDay($recurrence->dayofweek); + break; + /* TODO: Not sure about these 'Nth' rules - might need more eyes */ + case Horde_ActiveSync_Message_Recurrence::TYPE_YEARLY: + $rrule->setRecurType(Horde_Date_Recurrence::RECUR_YEARLY_DATE); + break; + case Horde_ActiveSync_Message_Recurrence::TYPE_YEARLYNTH: + $rrule->setRecurType(Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY); + $rrule->setRecurOnDay($recurrence->dayofweek); + break; + } + + if ($rcnt = $recurrence->occurrences) { + $rrule->setRecurCount($rcnt); + } + if ($runtil = $recurrence->until) { + $rrule->setRecurEnd(new Horde_Date($runtil)); + } + if ($interval = $recurrence->interval) { + $rrule->setRecurInterval($interval); + } + + return $rrule; + } + + /** + * Add a recurrence exception + * + * @param Horde_ActiveSync_Message_Exception $exception + */ + public function addException(Horde_ActiveSync_Message_Exception $exception) + { +// if (!isset($this->_properties['exceptions']) || !is_array($this->_properties['exceptions'])) { +// $this->_properties['exceptions'] = array(); +// } + $this->exceptions[] = $exception; + //$this->_properties['exceptions'][] = $exception; + } + + /** + * + * @return array An array of Horde_ActiveSync_Message_Exception objects + */ + public function getExceptions() + { + return $this->exceptions; + //return $this->_getAttribute('exceptions', array()); + } + + /** + * Set the sensitivity level for this appointment. + * + * Should be one of: + * normal, personal, private, confidential + * + * @param string $sensitivity + */ + public function setSensitivity($sensitivity) + { + switch ($sensitivity) { + case 'normal': + $sensitivity = self::SENSITIVITY_NORMAL; + break; + case 'personal': + $sensitivity = self::SENSITIVITY_PERSONAL; + break; + case 'private': + $sensitivity = self::SENSITIVITY_PRIVATE; + break; + case 'confidential': + $sensitivity = self::SENSITIVITY_CONFIDENTIAL; + break; + default: + return; + } + + $this->_properties['sensitivity'] = $sensitivity; + } + + /** + * Return the sensitivity setting for this appointment + * + * @return string One of: normal, personal, private, confidential + */ + public function getSensitivity() + { + switch ($this->_getAttribute('sensitivity')) { + case self::SENSITIVITY_NORMAL: + return 'normal'; + break; + case self::SENSITIVITY_PERSONAL: + return 'personal'; + break; + case self::SENSITIVITY_PRIVATE: + return 'private'; + break; + case self::SENSITIVITY_CONFIDENTIAL: + return 'confidential'; + break; + default: + return; + } + } + + /** + * Sets the busy status for this appointment + * + * Should be one of: + * free, tenative, busy, out + * + * + * @param string $busy The busy status to use + */ + public function setBusyStatus($busy) + { + switch ($busy) { + case 'free': + $busy = self::BUSYSTATUS_FREE; + break; + case 'tenative': + $busy = self::BUSYSTATUS_TENATIVE; + break; + case 'busy': + $busy = self::BUSYSTATUS_BUSY; + break; + case 'out': + $busy = self::BUSYSTATUS_OUT; + break; + default: + return; + } + + $this->_properties['busystatus'] = $busy; + } + + /** + * Return the busy status for this appointment. + * + * @return string One of free, tenative, busy, out + */ + public function getBusyStatus() + { + switch ($this->_getAttribute('busystatus')) { + case self::BUSYSTATUS_FREE: + return 'free'; + break; + case self::BUSYSTATUS_TENATIVE: + return 'tenative'; + break; + case self::BUSYSTATUS_BUSY: + return 'busy'; + break; + case self::BUSYSTATUS_OUT: + return 'out'; + break; + default: + return; + } + } + + /** + * Set user response type. Should be one of: + * none, organizer, tenative, accepted, declined + * + * @param string $response The response type + */ + public function setResponseType($response) + { + switch ($response) { + case 'none': + $response = self::RESPONSE_NONE; + break; + case 'organizer': + $response = self::RESPONSE_ORGANIZER; + break; + case 'tenative': + $response = self::RESPONSE_TENATIVE; + break; + case 'accepted': + $response = self::RESPONSE_ACCEPTED; + break; + case 'declined': + $response = self::RESPONSE_DECLINED; + break; + default: + return; + } + + $this->_properties['responsetype'] = $response; + } + + /** + * Get response type + * + * @return string one of: none, organizer, tenatve, accepted, declined + */ + public function getResponseType() + { + switch ($this->_getAttribute('responsetype')) { + case self::RESPONSE_NONE: + return 'none'; + break; + case self::RESPONSE_ORGANIZER: + return 'organizer'; + break; + case self::RESPONSE_TENATIVE: + return 'tenative'; + break; + case self::RESPONSE_ACCEPTED: + return 'accepted'; + break; + case self::RESPONSE_DECLINED: + return 'declined'; + break; + default: + return; + } + } + + /** + * Set reminder for this appointment. + * + * @param integer $minutes The number of minutes before appintment to + * trigger a reminder. + */ + public function setReminder($minutes) + { + $this->_properties['reminder'] = (int)$minutes; + } + + /** + * + * @return integer Number of minutes before appointment for notifications. + */ + public function getReminder() + { + return $this->_getAttribute('reminder'); + } + + /** + * Set the status for this appointment. Should be one of: + * none, meeting, received, canceled, canceledreceived. + * + * TODO: Not really sure about when these would be used. + * + * + * @param $status + */ + public function setMeetingStatus($status) + { + switch ($status) { + case 'none': + $status = self::MEETING_NOT_MEETING; + break; + case 'meeting': + $status = self::MEETING_IS_MEETING; + break; + case 'received': + $status = self::MEETING_RECEIVED; + break; + case 'canceled': + $status = self::MEETING_CANCELLED; + break; + default: + return; + } + + $this->_properties['meetingstatus'] = $status; + } + + /** + * + * @return string One of none, meeting, received, canceled + */ + public function getMeetingStatus() + { + switch ($this->_getAttribute('meetingstatus')) { + case self::MEETING_NOT_MEETING: + return 'none'; + break; + case self::MEETING_IS_MEETING: + return 'meeting'; + break; + case self::MEETING_RECEIVED: + return 'received'; + break; + case self::MEETING_CANCELLED: + return 'canceled'; + break; + default: + return; + } + } + + /** + * Add an attendee to this appointment + * + * @param array $attendee 'name', 'email' for each attendee + */ + public function addAttendee($attendee) + { + if (!isset($this->_properties['attendees']) || !is_array($this->_properties['attendees'])) { + $this->_properties['attendees'] = array(); + } + + /* Both email and name are REQUIRED if setting an attendee */ + $this->_properties['attendees'][] = $attendee; + } + + /** + * + * @return array An array of 'name' and 'email' hashes + */ + public function getAttendees() + { + return $this->_getAttribute('attendees'); + } + + /** + * TODO + * + * @param $body + */ + public function setBody($body) + { + + } + + public function getBody() + { + + } + + /** + * Add a category to the appointment + * + * TODO + * + * @param string $category + */ + public function addCategory($category) + { + + } + + public function getCategory() + { + + } + + /** + * Return the collection class name the object is for. + * + * @return string + */ + public function getClass() + { + return 'Calendar'; + } + + protected function _getAttribute($name, $default = null) + { + if (!empty($this->_properties[$name])) { + return $this->_properties[$name]; + } else { + return $default; + } + } +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Attendee.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Attendee.php new file mode 100644 index 000000000..a71db96fb --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Attendee.php @@ -0,0 +1,27 @@ + + * @package Horde_ActiveSync + */ +class Horde_ActiveSync_Message_Attendee extends Horde_ActiveSync_Message_Base +{ + /** + * Const'r + * + * @param array $params + */ + function __construct($params) { + $mapping = array( + SYNC_POOMCAL_EMAIL => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'email'), + SYNC_POOMCAL_NAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'name' ) + ); + + parent::__construct($mapping, $params); + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php new file mode 100644 index 000000000..fe25350f1 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php @@ -0,0 +1,384 @@ + + * @package Horde_ActiveSync + */ +/*********************************************** +* File : streamer.php +* Project : Z-Push +* Descr : This file handles streaming of +* WBXML objects. It must be +* subclassed so the internals of +* the object can be specified via +* $mapping. Basically we set/read +* the object variables of the +* subclass according to the mappings +* +* +* Created : 01.10.2007 +* +* � Zarafa Deutschland GmbH, www.zarafaserver.de +* This file is distributed under GPL v2. +* Consult LICENSE file for details +************************************************/ +class Horde_ActiveSync_Message_Base +{ + + /* Attribute Keys */ + const KEY_ATTRIBUTE = 1; + const KEY_VALUES = 2; + const KEY_TYPE = 3; + + /* Types */ + const TYPE_DATE = 1; + const TYPE_HEX = 2; + const TYPE_DATE_DASHES = 3; + const TYPE_MAPI_STREAM = 4; + + /** + * Holds the mapping for SYNC_POOMCAL_* -> object properties + * + * @var array + */ + protected $_mapping; + + /** + * Holds property values + * + * @array + */ + protected $_properties = array(); + + /** + * Message flags + * //FIXME: use accessor methods, make this protected + * @var Horde_ActiveSync_FLAG_* constant + */ + public $flags; + + /** + * Logger + * + * @var Horde_Log_Logger + */ + protected $_logger; + + /** + * Const'r + * + * @param array $mapping A mapping array from constants -> property names + * @param array $options Any addition options the message may require + * + * @return Horde_ActiveSync_Message_Base + */ + public function __construct($mapping, $options) + { + $this->_mapping = $mapping; + $this->flags = false; + if (!empty($options['logger'])) { + $this->_logger = $options['logger']; + } + } + + public function __get($property) + { + if (!empty($this->_properties[$property])) { + return $this->_properties[$property]; + } else { + return ''; + } + } + + public function __set($property, $value) + { + $this->_properties[$property] = $value; + } + + public function __isset($property) + { + return !empty($this->_properties[$property]); + } + + /** + * Recursively decodes the WBXML from input stream. This means that if this + * message contains complex types (like Appointment.Recuurence for example) + * the sub-objects are auto-instantiated and decoded as well. Places the + * decoded objects in the local properties array. + * + * @param Horde_ActiveSync_Wbxml_Decoder The stream decoder + * + * @throws Horde_ActiveSync_Exception + * @return void + */ + public function decodeStream(Horde_ActiveSync_Wbxml_Decoder &$decoder) + { + while (1) { + $entity = $decoder->getElement(); + + if ($entity[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG) { + if (! ($entity[Horde_ActiveSync_Wbxml::EN_FLAGS] & Horde_ActiveSync_Wbxml::EN_FLAGS_CONTENT)) { + $map = $this->_mapping[$entity[Horde_ActiveSync_Wbxml::EN_TAG]]; + if (!isset($map[Horde_ActiveSync_Message_Base::KEY_TYPE])) { + $this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE] = ''; + } elseif ($map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_DATE || $map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_DATE_DASHES ) { + $this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE] = ''; + } + continue; + } + + /* Found start tag */ + if (!isset($this->_mapping[$entity[Horde_ActiveSync_Wbxml::EN_TAG]])) { + $this->_logDebug('Tag ' . $entity[Horde_ActiveSync_Wbxml::EN_TAG] . ' unexpected in type XML type ' . get_class($this)); + throw new Horde_ActiveSync_Exception('Unexpected tag'); + } else { + $map = $this->_mapping[$entity[Horde_ActiveSync_Wbxml::EN_TAG]]; + + /* Handle arrays of attribute values */ + if (isset($map[Horde_ActiveSync_Message_Base::KEY_VALUES])) { + while (1) { + if (!$decoder->getElementStartTag($map[Horde_ActiveSync_Message_Base::KEY_VALUES])) { + break; + } + if (isset($map[Horde_ActiveSync_Message_Base::KEY_TYPE])) { + $decoded = new $map[Horde_ActiveSync_Message_Base::KEY_TYPE]; + $decoded->decodeStream($decoder); + } else { + $decoded = $decoder->getElementContent(); + } + + if (!isset($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE])) { + $this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE] = array($decoded); + } else { + array_push($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE], $decoded); + } + + if (!$decoder->getElementEndTag()) { + throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag'); + } + } + + if (!$decoder->getElementEndTag()) { + return false; + } + } else { + /* Handle a simple attribute value */ + if (isset($map[Horde_ActiveSync_Message_Base::KEY_TYPE])) { + /* Complex type, decode recursively */ + if ($map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_DATE || $map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_DATE_DASHES) { + $decoded = self::_parseDate($decoder->getElementContent()); + if (!$decoder->getElementEndTag()) { + throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag'); + } + } elseif ($map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_HEX) { + $decoded = self::hex2bin($decoder->getElementContent()); + if (!$decoder->getElementEndTag()) { + throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag'); + } + } else { + $subdecoder = new $map[Horde_ActiveSync_Message_Base::KEY_TYPE](); + if ($subdecoder->decodeStream($decoder) === false) { + throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag'); + } + + $decoded = $subdecoder; + if (!$decoder->getElementEndTag()) { + $this->_logError('No end tag for ' . $entity[Horde_ActiveSync_Wbxml::EN_TAG]); + throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag'); + } + } + } else { + /* Simple type, just get content */ + $decoded = $decoder->getElementContent(); + if ($decoded === false) { + $this->_logError('Unable to get content for ' . $entity[Horde_ActiveSync_Wbxml::EN_TAG]); + throw new Horde_ActiveSync_Exception('Unknown parsing error.'); + } + + if (!$decoder->getElementEndTag()) { + $this->_logError('Unable to get end tag for ' . $entity[Horde_ActiveSync_Wbxml::EN_TAG]); + throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag'); + } + } + /* $decoded now contains data object (or string) */ + $this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE] = $decoded; + } + } + } elseif ($entity[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) { + $decoder->_ungetElement($entity); + break; + } else { + $this->_logError('Unexpected content in type'); + break; + } + } + } + + /** + * Decodes a wbxml string into this object's properties. + * + * @param string $wbxml + */ + public function decode($wbxml) + { + throw new Horde_ActiveSync_Exception('Not implemented.'); + } + + /** + * Encodes this message object into a wbxml string. + * + * @return string wbxml string + */ + public function encode() + { + throw new Horde_ActiveSync_Exception('Not Implemented.'); + } + + /** + * Encodes this object (and any sub-objects) as wbxml to the output stream. + * Output is ordered according to $_mapping + * + * @param Horde_ActiveSync_Wbxml_Encoder $encoder The wbxml stream encoder + * + * @return void + */ + public function encodeStream(Horde_ActiveSync_Wbxml_Encoder &$encoder) + { + foreach ($this->_mapping as $tag => $map) { + if (isset($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE])) { + /* Variable is available */ + if (is_object($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE]) && !($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE] instanceof Horde_Date)) { + /* Subobjects can do their own encoding */ + $encoder->startTag($tag); + $this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE]->encodeStream($encoder); + $encoder->endTag(); + } elseif (isset($map[Horde_ActiveSync_Message_Base::KEY_VALUES]) && is_array($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE])) { + /* Array of objects */ + $encoder->startTag($tag); // Outputs array container (eg Attachments) + foreach ($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE] as $element) { + if (is_object($element)) { + // Outputs object container (eg Attachment) + $encoder->startTag($map[Horde_ActiveSync_Message_Base::KEY_VALUES]); + $element->encodeStream($encoder); + $encoder->endTag(); + } else { + if(strlen($element) == 0) { + // Do not output empty items. Not sure if we + // should output an empty tag with + // $encoder->startTag($map[Horde_ActiveSync_Message_Base::KEY_VALUES], false, true); + } else { + $encoder->startTag($map[Horde_ActiveSync_Message_Base::KEY_VALUES]); + $encoder->content($element); + $encoder->endTag(); + } + } + } + $encoder->endTag(); + } else { + /* Simple type */ + if (strlen($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE]) == 0) { + // Do not output empty items. + // See above: $encoder->startTag($tag, false, true); + continue; + } else { + $encoder->startTag($tag); + } + if (isset($map[Horde_ActiveSync_Message_Base::KEY_TYPE]) && ($map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_DATE || $map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_DATE_DASHES)) { + if (!empty($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE])) { // don't output 1-1-1970 + $encoder->content(self::_formatDate($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE], $map[Horde_ActiveSync_Message_Base::KEY_TYPE])); + } + } elseif (isset($map[Horde_ActiveSync_Message_Base::KEY_TYPE]) && $map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_HEX) { + $encoder->content(bin2hex($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE])); + } elseif (isset($map[Horde_ActiveSync_Message_Base::KEY_TYPE]) && $map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_MAPI_STREAM) { + $encoder->content($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE]); + } else { + $encoder->content($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE]); + } + $encoder->endTag(); + } + } + } + } + + /** + * + * @param $message + * @return unknown_type + */ + protected function _logDebug($message) + { + if (!empty($this->_logger)) { + $this->_logger->debug($message); + } + } + + /** + * + * @param $message + * @return unknown_type + */ + protected function _logError($message) + { + if (!empty($this->_logger)) { + $this->_logger->error($message); + } + } + + /** + * Oh yeah. This is beautiful. Exchange outputs date fields differently in + * calendar items and emails. We could just always send one or the other, + * but unfortunately nokia's 'Mail for exchange' depends on this quirk. + * So we have to send a different date type depending on where it's used. + * + * @param Horde_Date $dt The datetime to format (assumed to be in local tz) + * @param Constant $type The type to format as (TYPE_DATE or TYPE_DATE_DASHES) + * + * @return string The formatted date + */ + static protected function _formatDate($dt, $type) + { + if ($type == Horde_ActiveSync_Message_Base::TYPE_DATE) { + return $dt->setTimezone('UTC')->format('Ymd\THis\Z'); + } elseif ($type == Horde_ActiveSync_Message_Base::TYPE_DATE_DASHES) { + return $dt->setTimezone('UTC')->format('Y-m-d\TH:i:s\.000\Z'); + } + } + + /** + * Get a Horde_Date from a timestamp, ensuring it's in the correct format. + * + * @param string $ts + * + * @return Horde_Date + */ + static protected function _parseDate($ts) + { + if (preg_match("/(\d{4})[^0-9]*(\d{2})[^0-9]*(\d{2})T(\d{2})[^0-9]*(\d{2})[^0-9]*(\d{2})(.\d+)?Z/", $ts, $matches)) { + return new Horde_Date($ts); + } + + throw new Horde_ActiveSync_Exception('Invalid date format'); + } + + /** + * Function which converts a hex entryid to a binary entryid. + * @param string @data the hexadecimal string + */ + static private function hex2bin($data) + { + $len = strlen($data); + $newdata = ""; + + for($i = 0;$i < $len;$i += 2) + { + $newdata .= pack("C", hexdec(substr($data, $i, 2))); + } + return $newdata; + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Contact.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Contact.php new file mode 100644 index 000000000..388b957d6 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Contact.php @@ -0,0 +1,151 @@ + + * @package Horde_ActiveSync + */ +class Horde_ActiveSync_Message_Contact extends Horde_ActiveSync_Message_Base +{ + public $anniversary; + public $assistantname; + public $assistnamephonenumber; + public $birthday; + public $body; + public $bodysize; + public $bodytruncated; + public $business2phonenumber; + public $businesscity; + public $businesscountry; + public $businesspostalcode; + public $businessstate; + public $businessstreet; + public $businessfaxnumber; + public $businessphonenumber; + public $carphonenumber; + public $categories = array(); + public $children = array(); + public $companyname; + public $department; + public $email1address; + public $email2address; + public $email3address; + public $fileas; + public $firstname; + public $home2phonenumber; + public $homecity; + public $homecountry; + public $homepostalcode; + public $homestate; + public $homestreet; + public $homefaxnumber; + public $homephonenumber; + public $jobtitle; + public $lastname; + public $middlename; + public $mobilephonenumber; + public $officelocation; + public $othercity; + public $othercountry; + public $otherpostalcode; + public $otherstate; + public $otherstreet; + public $pagernumber; + public $radiophonenumber; + public $spouse; + public $suffix; + public $title; + public $webpage; + public $yomicompanyname; + public $yomifirstname; + public $yomilastname; + public $rtf; + public $picture; + public $nickname; + + public function __construct($params = array()) + { + $mapping = array ( + SYNC_POOMCONTACTS_ANNIVERSARY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'anniversary', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE_DASHES ), + SYNC_POOMCONTACTS_ASSISTANTNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'assistantname'), + SYNC_POOMCONTACTS_ASSISTNAMEPHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'assistnamephonenumber'), + SYNC_POOMCONTACTS_BIRTHDAY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'birthday', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE_DASHES ), + SYNC_POOMCONTACTS_BODY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'body'), + SYNC_POOMCONTACTS_BODYSIZE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'bodysize'), + SYNC_POOMCONTACTS_BODYTRUNCATED => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'bodytruncated'), + SYNC_POOMCONTACTS_BUSINESS2PHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'business2phonenumber'), + SYNC_POOMCONTACTS_BUSINESSCITY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businesscity'), + SYNC_POOMCONTACTS_BUSINESSCOUNTRY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businesscountry'), + SYNC_POOMCONTACTS_BUSINESSPOSTALCODE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businesspostalcode'), + SYNC_POOMCONTACTS_BUSINESSSTATE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businessstate'), + SYNC_POOMCONTACTS_BUSINESSSTREET => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businessstreet'), + SYNC_POOMCONTACTS_BUSINESSFAXNUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businessfaxnumber'), + SYNC_POOMCONTACTS_BUSINESSPHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businessphonenumber'), + SYNC_POOMCONTACTS_CARPHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'carphonenumber'), + SYNC_POOMCONTACTS_CHILDREN => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'children', Horde_ActiveSync_Message_Base::KEY_VALUES => SYNC_POOMCONTACTS_CHILD ), + SYNC_POOMCONTACTS_COMPANYNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'companyname'), + SYNC_POOMCONTACTS_DEPARTMENT => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'department'), + SYNC_POOMCONTACTS_EMAIL1ADDRESS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'email1address'), + SYNC_POOMCONTACTS_EMAIL2ADDRESS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'email2address'), + SYNC_POOMCONTACTS_EMAIL3ADDRESS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'email3address'), + SYNC_POOMCONTACTS_FILEAS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'fileas'), + SYNC_POOMCONTACTS_FIRSTNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'firstname'), + SYNC_POOMCONTACTS_HOME2PHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'home2phonenumber'), + SYNC_POOMCONTACTS_HOMECITY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homecity'), + SYNC_POOMCONTACTS_HOMECOUNTRY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homecountry'), + SYNC_POOMCONTACTS_HOMEPOSTALCODE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homepostalcode'), + SYNC_POOMCONTACTS_HOMESTATE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homestate'), + SYNC_POOMCONTACTS_HOMESTREET => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homestreet'), + SYNC_POOMCONTACTS_HOMEFAXNUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homefaxnumber'), + SYNC_POOMCONTACTS_HOMEPHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homephonenumber'), + SYNC_POOMCONTACTS_JOBTITLE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'jobtitle'), + SYNC_POOMCONTACTS_LASTNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'lastname'), + SYNC_POOMCONTACTS_MIDDLENAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'middlename'), + SYNC_POOMCONTACTS_MOBILEPHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'mobilephonenumber'), + SYNC_POOMCONTACTS_OFFICELOCATION => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'officelocation'), + SYNC_POOMCONTACTS_OTHERCITY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'othercity'), + SYNC_POOMCONTACTS_OTHERCOUNTRY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'othercountry'), + SYNC_POOMCONTACTS_OTHERPOSTALCODE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'otherpostalcode'), + SYNC_POOMCONTACTS_OTHERSTATE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'otherstate'), + SYNC_POOMCONTACTS_OTHERSTREET => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'otherstreet'), + SYNC_POOMCONTACTS_PAGERNUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'pagernumber'), + SYNC_POOMCONTACTS_RADIOPHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'radiophonenumber'), + SYNC_POOMCONTACTS_SPOUSE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'spouse'), + SYNC_POOMCONTACTS_SUFFIX => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'suffix'), + SYNC_POOMCONTACTS_TITLE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'title'), + SYNC_POOMCONTACTS_WEBPAGE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'webpage'), + SYNC_POOMCONTACTS_YOMICOMPANYNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'yomicompanyname'), + SYNC_POOMCONTACTS_YOMIFIRSTNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'yomifirstname'), + SYNC_POOMCONTACTS_YOMILASTNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'yomilastname'), + SYNC_POOMCONTACTS_RTF => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'rtf'), + SYNC_POOMCONTACTS_PICTURE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'picture'), + SYNC_POOMCONTACTS_CATEGORIES => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'categories', Horde_ActiveSync_Message_Base::KEY_VALUES => SYNC_POOMCONTACTS_CATEGORY ), + ); + + /* Additional mappings for AS versions >= 2.5 */ + if (isset($params['protocolversion']) && $params['protocolversion'] >= 2.5) { + $mapping += array( + SYNC_POOMCONTACTS2_CUSTOMERID => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'customerid'), + SYNC_POOMCONTACTS2_GOVERNMENTID => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'governmentid'), + SYNC_POOMCONTACTS2_IMADDRESS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'imaddress'), + SYNC_POOMCONTACTS2_IMADDRESS2 => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'imaddress2'), + SYNC_POOMCONTACTS2_IMADDRESS3 => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'imaddress3'), + SYNC_POOMCONTACTS2_MANAGERNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'managername'), + SYNC_POOMCONTACTS2_COMPANYMAINPHONE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'companymainphone'), + SYNC_POOMCONTACTS2_ACCOUNTNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'accountname'), + SYNC_POOMCONTACTS2_NICKNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'nickname'), + SYNC_POOMCONTACTS2_MMS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'mms'), + ); + } + + parent::__construct($mapping, $params); + } + + public function getClass() + { + return 'Contacts'; + } +} diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Exception.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Exception.php new file mode 100644 index 000000000..ea79df65b --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Exception.php @@ -0,0 +1,52 @@ + + * @package Horde_ActiveSync + */ +class Horde_ActiveSync_Message_Exception extends Horde_ActiveSync_Message_Appointment +{ + /** + * Constructor + * + * @param array $params + * + * @return Horde_ActiveSync_Message_Appointment + */ + public function __construct($params = array()) + { + parent::__construct($params); + + /* Some additional properties for Exceptions */ + $this->_mapping[SYNC_POOMCAL_EXCEPTIONSTARTTIME] = array( + Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'exceptionstarttime', + Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE); + + $this->_mapping[SYNC_POOMCAL_DELETED] = array(Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'deleted'); + } + + /** + * Sets the DELETED field on this exception + * + * @param boolean $flag + */ + public function setDeletedFlag($flag) + { + $this->_properties['deleted'] = $flag; + } + + /** + * Exception start time + * + * @return Horde_Date The exception's start time + */ + public function getExceptionStartTime() + { + return $this->_getAttribute('exceptionstarttime'); + } + +} diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Folder.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Folder.php new file mode 100644 index 000000000..d5929949b --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Folder.php @@ -0,0 +1,34 @@ + + * @package Horde_ActiveSync + */ +class Horde_ActiveSync_Message_Folder extends Horde_ActiveSync_Message_Base +{ + public $serverid; + public $parentid; + public $displayname; + public $type; + + public function __construct($params = array()) + { + $mapping = array ( + SYNC_FOLDERHIERARCHY_SERVERENTRYID => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'serverid'), + SYNC_FOLDERHIERARCHY_PARENTID => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'parentid'), + SYNC_FOLDERHIERARCHY_DISPLAYNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'displayname'), + SYNC_FOLDERHIERARCHY_TYPE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'type') + ); + + parent::__construct($mapping, $params); + } + + public function getClass() + { + return 'Folders'; + } +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Recurrence.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Recurrence.php new file mode 100644 index 000000000..f0dae43f6 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Recurrence.php @@ -0,0 +1,48 @@ + + * @package Horde_ActiveSync + */ +class Horde_ActiveSync_Message_Recurrence extends Horde_ActiveSync_Message_Base +{ + public $type; + public $until; + public $occurrences; + public $interval; + public $dayofweek; + public $dayofmonth; + public $weekofmonth; + public $monthofyear; + + /* MS AS Recurrence types */ + const TYPE_DAILY = 0; + const TYPE_WEEKLY = 1; + const TYPE_MONTHLY = 2; + const TYPE_MONTHLY_NTH = 3; + const TYPE_YEARLY = 5; + const TYPE_YEARLYNTH = 6; + + + + function __construct($params = array()) + { + $mapping = array ( + SYNC_POOMCAL_TYPE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'type'), + SYNC_POOMCAL_UNTIL => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'until', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE), + SYNC_POOMCAL_OCCURRENCES => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'occurrences'), + SYNC_POOMCAL_INTERVAL => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'interval'), + SYNC_POOMCAL_DAYOFWEEK => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'dayofweek'), + SYNC_POOMCAL_DAYOFMONTH => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'dayofmonth'), + SYNC_POOMCAL_WEEKOFMONTH => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'weekofmonth'), + SYNC_POOMCAL_MONTHOFYEAR => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'monthofyear') + ); + + parent::__construct($mapping, $params); + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Base.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Base.php new file mode 100644 index 000000000..4812255fa --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Base.php @@ -0,0 +1,130 @@ + + * @package Horde_ActiveSync + */ +/** + * Zarafa Deutschland GmbH, www.zarafaserver.de + * This file is distributed under GPL v2. + * Consult LICENSE file for details + */ +abstract class Horde_ActiveSync_Request_Base +{ + /** + * Driver for communicating with the backend datastore. + * + * @var Horde_ActiveSync_Driver_Base + */ + protected $_driver; + + /** + * Encoder + * + * @var Horde_ActiveSync_Wbxml_Encoder + */ + protected $_encoder; + + /** + * Decoder + * + * @var Horde_ActiveSync_Wbxml_Decoder + */ + protected $_decoder; + + /** + * Request object + * + * @var Horde_Controller_Request_Http + */ + protected $_request; + + /** + * Whether we require provisioned devices. + * Valid values are true, false, or loose. + * Loose allows devices that don't know about provisioning to continue to + * function, but requires devices that are capable to be provisioned. + * + * @var mixed + */ + protected $_provisioning = false; + + /** + * The ActiveSync Version + * + * @var string + */ + protected $_version; + + /** + * The device Id + * + * @var string + */ + protected $_devId; + + /** + * Used to track what error code to send back to PIM on failure + * + * @var integer + */ + protected $_statusCode = 0; + + protected $_logger; + + /** + * Const'r + * + * @param Horde_ActiveSync_Driver $driver The backend driver + * @param Horde_ActiveSync_Wbxml_Decoder $decoder The Wbxml decoder + * @param Horde_ActiveSync_Wbxml_Endcodder $encdoer The Wbxml encoder + * @param Horde_Controller_Request_Http $request The request object + * @param string $version ActiveSync version + * @param string $devId The PIM device id + * @param string $provisioning Is provisioning required? + * + * @return Horde_ActiveSync + */ + public function __construct(Horde_ActiveSync_Driver_Base $driver, + Horde_ActiveSync_Wbxml_Decoder $decoder, + Horde_ActiveSync_Wbxml_Encoder $encoder, + Horde_Controller_Request_Http $request, + $version, $devId, $provisioning) + { + /* Backend driver */ + $this->_driver = $driver; + + /* Wbxml handlers */ + $this->_encoder = $encoder; + $this->_decoder = $decoder; + + /* The http request */ + $this->_request = $request; + + /* Protocol Version */ + $this->_version = $version; + + /* Device Id */ + $this->_devId = $devId; + + /* Provisioning support */ + $this->_provisioning = $provisioning; + + /* Logger */ + $this->_logger; + } + + public function setLogger(Horde_Log_Logger $logger) { + $this->_logger = $logger; + } + /** + * + * @param string $version + * @param string $devId + */ + abstract public function handle(Horde_ActiveSync $activeSync); + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php new file mode 100644 index 000000000..7a59b46e9 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php @@ -0,0 +1,214 @@ + + * @package Horde_ActiveSync + */ +/** + * Zarafa Deutschland GmbH, www.zarafaserver.de + * This file is distributed under GPL v2. + * Consult LICENSE file for details + */ +class Horde_ActiveSync_Request_FolderSync extends Horde_ActiveSync_Request_Base +{ + /* SYNC Status response codes */ + const STATUS_SUCCESS = 1; + const STATUS_SERVERERROR = 6; // Should probably return to synckey 0? + const STATUS_TIMEOUT = 8; + const STATUS_KEYMISM = 9; + const STATUS_PROTOERR = 10; + + public function handle(Horde_ActiveSync $activeSync) + { + /* Be optomistic */ + $this->_statusCode = self::STATUS_SUCCESS; + $this->_logger->info('[Horde_ActiveSync::handleFolderSync] Beginning FOLDERSYNC'); + + /* Maps serverid -> clientid for items that are received from the PIM */ + $map = array(); + + /* Start parsing input */ + if (!$this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC)) { + $this->_logger->err('[Horde_ActiveSync::handleFolderSync] No input to parse'); + $this->_statusCode = self::STATUS_PROTOERR; + $this->_handleError(); + exit; + } + + /* Get the current synckey from PIM */ + if (!$this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SYNCKEY)) { + $this->_logger->err('[Horde_ActiveSync::handleFolderSync] No input to parse'); + $this->_statusCode = self::STATUS_PROTOERR; + $this->_handleError(); + exit; + } + $synckey = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + $this->_logger->err('[Horde_ActiveSync::handleFolderSync] No input to parse'); + $this->_statusCode = self::STATUS_PROTOERR; + $this->_handleError(); + exit; + } + $this->_logger->debug('[Horde_ActiveSync::handleFolderSync] syncKey: ' . $synckey); + + /* Initialize state engine */ + $state = &$this->_driver->getStateObject(array('synckey' => $synckey)); + try { + /* Get folders that we know about already */ + $state->loadState($synckey); + } catch (Horde_ActiveSync_Exception $e) { + $this->_statusCode = self::STATUS_KEYMISM; + $this->_handleError(); + exit; + } + $seenfolders = $state->getKnownFolders(); + + /* Get new synckey to send back */ + $newsynckey = $state->getNewSyncKey($synckey); + $this->_logger->debug('[Horde_ActiveSync::handleFolderSync] newSyncKey: ' . $newsynckey); + + /* Deal with folder hierarchy changes */ + if ($this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_CHANGES)) { + // Ignore if present + if ($this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_COUNT)) { + $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + $this->_statusCode = self::STATUS_PROTOERR; + $this->_handleError(); + exit; + } + } + + /* Process the incoming changes to folders */ + $element = $this->_decoder->getElement(); + if ($element[Horde_ActiveSync_Wbxml::EN_TYPE] != Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG) { + $this->_statusCode = self::STATUS_PROTOERR; + $this->_handleError(); + exit; + } + + /* Configure importer with last state */ + $importer = $this->_driver->getImporter(); + $importer->init($state, false, $synckey); + + while (1) { + $folder = new Horde_ActiveSync_Message_Folder(array('logger' => $this->_logger)); + if (!$folder->decodeStream($this->_decoder)) { + break; + } + + switch ($element[Horde_ActiveSync_Wbxml::EN_TAG]) { + case SYNC_ADD: + case SYNC_MODIFY: + $serverid = $importer->ImportFolderChange($folder); + break; + case SYNC_REMOVE: + $serverid = $importer->ImportFolderDeletion($folder); + break; + } + + /* Update the map */ + if ($serverid) { + // FIXME: Yet Another property used, but never defined + $map[$serverid] = $folder->clientid; + } + } + + if (!$this->_decoder->getElementEndTag()) { + $this->_statusCode = self::STATUS_PROTOERR; + $this->_handleError(); + exit; + } + } + + if (!$this->_decoder->getElementEndTag()) { + $this->_statusCode = self::STATUS_PROTOERR; + $this->_handleError(); + exit; + } + + /* Start sending server -> PIM changes */ + $this->_logger->debug('[Horde_ActiveSync::handleFolderSync] Preparing to send changes to PIM'); + + // The $importer caches all imports in-memory, so we can send a change + // count before sending the actual data. As the amount of data done in + // this operation is rather low, this is not memory problem. Note that + // this is not done when sync'ing messages - we let the exporter write + // directly to WBXML. + // TODO: Combine all these import caches into a single Class + $importer = new Horde_ActiveSync_HierarchyCache(); + $exporter = $this->_driver->GetExporter(); + $exporter->init($state, $importer, array('synckey' => $synckey)); + + /* Perform the actual sync operation */ + while(is_array($exporter->syncronize())); + + // Output our WBXML reply now + $this->_encoder->StartWBXML(); + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC); + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); + $this->_encoder->content($this->_statusCode); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); + $this->_encoder->content($newsynckey); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_CHANGES); + + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_COUNT); + $this->_encoder->content($importer->count); + $this->_encoder->endTag(); + + if (count($importer->changed) > 0) { + foreach ($importer->changed as $folder) { + if (isset($folder->serverid) && in_array($folder->serverid, $seenfolders)) { + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_UPDATE); + } else { + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_ADD); + } + $folder->encodeStream($this->_encoder); + $this->_encoder->endTag(); + } + } + + if (count($importer->deleted) > 0) { + foreach ($importer->deleted as $folder) { + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_REMOVE); + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_SERVERENTRYID); + $this->_encoder->content($folder); + $this->_encoder->endTag(); + $this->_encoder->endTag(); + } + } + + $this->_encoder->endTag(); + $this->_encoder->endTag(); + + /* Save the state as well as the known folder cache */ + $state->setNewSyncKey($newsynckey); + $state->save(); + + return true; + } + + /** + * Helper function for sending error responses + * + */ + private function _handleError() + { + $this->_encoder->StartWBXML(); + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC); + $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); + $this->_encoder->content($this->_statusCode); + $this->_encoder->endTag(); + $this->_encoder->endTag(); + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Options.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Options.php new file mode 100644 index 000000000..b28cea8f6 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Options.php @@ -0,0 +1,26 @@ + + * @package Horde_ActiveSync + */ +/** + * Zarafa Deutschland GmbH, www.zarafaserver.de + * This file is distributed under GPL v2. + * Consult LICENSE file for details + */ +class Horde_ActiveSync_Request_Options extends Horde_ActiveSync_Request_Base +{ + public function handle(Horde_ActiveSync $activeSync) + { + Horde_ActiveSync::activeSyncHeader(); + Horde_ActiveSync::versionHeader(); + Horde_ActiveSync::commandsHeader(); + + return true; + } + +} diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Ping.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Ping.php new file mode 100644 index 000000000..162f0c30b --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Ping.php @@ -0,0 +1,183 @@ + + * @package Horde_ActiveSync + */ +/** + * Zarafa Deutschland GmbH, www.zarafaserver.de + * This file is distributed under GPL v2. + * Consult LICENSE file for details + */ +class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base +{ + const STATUS_NOCHANGES = 1; + const STATUS_NEEDSYNC = 2; + const STATUS_MISSING = 3; + const STATUS_PROTERROR = 4; + // Hearbeat out of bounds (TODO) + const STATUS_HBOUTOFBOUNDS = 5; + + // Requested more then the max folders (TODO) + const STATUS_MAXFOLDERS = 6; + + // Folder sync is required, hierarchy out of date. + const STATUS_FOLDERSYNCREQD = 7; + const STATUS_SERVERERROR = 8; + + /** + * Handle a PING command from the PIM. Ping is sent periodically by the PIM + * to tell the server what folders we are interested in monitoring for + * changes. If no changes are detected by the server during the 'heartbeat' + * interval, the server sends back a status of 1 to indicate heartbeat + * expired and the client should re-issue the PING command. If a change + * has been found, the client is sent a 2 status and should then issue a + * SYNC command. + * + * @return boolean + */ + public function handle(Horde_ActiveSync $activeSync) + { + // FIXME + $timeout = 3; + $this->_logger->info('Ping received for: ' . $this->_devId); + + /* Glass half full kinda guy... */ + $this->_statusCode = self::STATUS_NOCHANGES; + + /* Initialize the state machine */ + $state = &$this->_driver->getStateObject(); + + /* See if we have an existing PING state. Need to do this here, before + * we read in the PING request since the PING request is allowed to omit + * sections if they have been sent previously */ + $collections = array_values($state->initPingState($this->_devId)); + $lifetime = $state->getPingLifetime(); + + /* Build the $collections array if we receive request from PIM */ + if ($this->_decoder->getElementStartTag(SYNC_PING_PING)) { + $this->_logger->debug('Ping init'); + if ($this->_decoder->getElementStartTag(SYNC_PING_LIFETIME)) { + $lifetime = $this->_decoder->getElementContent(); + $state->setPingLifetime($lifetime); + $this->_decoder->getElementEndTag(); + } + + if ($this->_decoder->getElementStartTag(SYNC_PING_FOLDERS)) { + $collections = array(); + while ($this->_decoder->getElementStartTag(SYNC_PING_FOLDER)) { + $collection = array(); + if ($this->_decoder->getElementStartTag(SYNC_PING_SERVERENTRYID)) { + $collection['id'] = $this->_decoder->getElementContent(); + $this->_decoder->getElementEndTag(); + } + if ($this->_decoder->getElementStartTag(SYNC_PING_FOLDERTYPE)) { + $collection['class'] = $this->_decoder->getElementContent(); + $this->_decoder->getElementEndTag(); + } + + $this->_decoder->getElementEndTag(); + array_push($collections, $collection); + } + + if (!$this->_decoder->getElementEndTag()) { + $this->_statusCode = self::STATUS_PROTERROR; + return false; + } + } + + if (!$this->_decoder->getElementEndTag()) { + $this->_statusCode = self::STATUS_PROTERROR; + return false; + } + } + + $changes = array(); + $dataavailable = false; + + /* Start waiting for changes, but only if we don't have any errors */ + if ($this->_statusCode == self::STATUS_NOCHANGES) { + $this->_logger->info(sprintf('Waiting for changes... (lifetime %d)', $lifetime)); + // FIXME + //for ($n = 0; $n < $lifetime / $timeout; $n++) { + for ($n = 0; $n < 10; $n++) { + //check the remote wipe status + if ($this->_provisioning === true) { + $rwstatus = $this->_driver->getDeviceRWStatus($this->_devId); + if ($rwstatus == SYNC_PROVISION_RWSTATUS_PENDING || $rwstatus == SYNC_PROVISION_RWSTATUS_WIPED) { + $this->_statusCode = self::STATUS_FOLDERSYNCREQD; + break; + } + } + + if (count($collections) == 0) { + $this->_logger->err('0 collections'); + $this->_statusCode = self::STATUS_MISSING; + break; + } + + for ($i = 0; $i < count($collections); $i++) { + $collection = $collections[$i]; + // Make sure we have the synckey (which is the devid for + // PING requests. + $collection['synckey'] = $this->_devId; + $exporter = $this->_driver->getExporter(); + $state->loadPingCollectionState($collection); + try { + $exporter->init($state, false, $collection); + } catch (Horde_ActiveSync_Exception $e) { + /* Stop ping if exporter cannot be configured */ + $this->_logger->err('Ping error: Exporter can not be configured. Waiting 30 seconds before ping is retried.'); + $n = $lifetime/ $timeout; + sleep(30); + break; + } + + $changecount = $exporter->GetChangeCount(); + if ($changecount > 0) { + $dataavailable = true; + $changes[$collection['id']] = $changecount; + $this->_statusCode = self::STATUS_NEEDSYNC; + } + + // Update the state, but don't bother with the backend since we + // are not updating any data. + while (is_array($exporter->syncronize(BACKEND_DISCARD_DATA))); + } + + if ($dataavailable) { + $this->_logger->info('Found changes'); + break; + } + sleep($timeout); + } + } + $this->_logger->debug('Starting StartWBXML output'); + $this->_encoder->StartWBXML(); + + $this->_encoder->startTag(SYNC_PING_PING); + + $this->_encoder->startTag(SYNC_PING_STATUS); + $this->_encoder->content($this->_statusCode); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_PING_FOLDERS); + foreach ($collections as $collection) { + if (isset($changes[$collection['id']])) { + $this->_encoder->startTag(SYNC_PING_FOLDER); + $this->_encoder->content($collection['id']); + $this->_encoder->endTag(); + } + } + $this->_encoder->endTag(); + + $this->_encoder->endTag(); + + $state->savePingState(); + + return true; + } +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Provision.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Provision.php new file mode 100644 index 000000000..86cb7b7f7 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Provision.php @@ -0,0 +1,173 @@ + + * @package Horde_ActiveSync + */ +/** + * Zarafa Deutschland GmbH, www.zarafaserver.de + * This file is distributed under GPL v2. + * Consult LICENSE file for details + */ +class Horde_ActiveSync_Request_Provision extends Horde_ActiveSync_Request_Base +{ + + public function handle(Horde_ActiveSync $activeSync) + { + $policykey = $activeSync->getPolicyKey(); + + $status = SYNC_PROVISION_STATUS_SUCCESS; + $phase2 = true; + if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_PROVISION)) { + return false; + } + + //handle android remote wipe. + if ($this->_decoder->getElementStartTag(SYNC_PROVISION_REMOTEWIPE)) { + if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_STATUS)) { + return false; + } + + $status = $this->_decoder->getElementContent(); + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + } else { + if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_POLICIES)) { + return false; + } + + if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_POLICY)) { + return false; + } + + if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_POLICYTYPE)) { + return false; + } + + $policytype = $this->_decoder->getElementContent(); + if ($policytype != 'MS-WAP-Provisioning-XML') { + $status = SYNC_PROVISION_STATUS_SERVERERROR; + } + if (!$this->_decoder->getElementEndTag()) {//policytype + return false; + } + + if ($this->_decoder->getElementStartTag(SYNC_PROVISION_POLICYKEY)) { + // This should be Phase 3 of the Provision conversation... + // We get the intermediate policy key sent back from the client. + // TODO: Still need to verify the key once we have some kind of + // storage for it. + $policykey = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_STATUS)) { + return false; + } + + $status = $this->_decoder->getElementContent(); + //do status handling + $status = SYNC_PROVISION_STATUS_SUCCESS; + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + $phase2 = false; + } + + if (!$this->_decoder->getElementEndTag()) {//policy + return false; + } + + if (!$this->_decoder->getElementEndTag()) {//policies + return false; + } + + if ($this->_decoder->getElementStartTag(SYNC_PROVISION_REMOTEWIPE)) { + if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_STATUS)) { + return false; + } + + $status = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + return false; + } + + if (!$this->_decoder->getElementEndTag()) { + return false; + } + } + } + + if (!$this->_decoder->getElementEndTag()) {//provision + return false; + } + $this->_encoder->StartWBXML(); + + // End of Phase 3 - We create the "final" policy key, store it, then + // send it to the client. + if (!$phase2) { + $policykey = $this->_driver->generatePolicyKey(); + $this->_driver->setPolicyKey($policykey, $this->_devId); + } + + $this->_encoder->startTag(SYNC_PROVISION_PROVISION); + + $this->_encoder->startTag(SYNC_PROVISION_STATUS); + $this->_encoder->content($status); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_PROVISION_POLICIES); + $this->_encoder->startTag(SYNC_PROVISION_POLICY); + + $this->_encoder->startTag(SYNC_PROVISION_POLICYTYPE); + $this->_encoder->content($policytype); + $this->_encoder->endTag(); + $this->_encoder->startTag(SYNC_PROVISION_STATUS); + $this->_encoder->content($status); + $this->_encoder->endTag(); + $this->_encoder->startTag(SYNC_PROVISION_POLICYKEY); + $this->_encoder->content($policykey); + $this->_encoder->endTag(); + if ($phase2) { + // If we are in Phase 2, send the security policies. + // TODO: Configure this! + $this->_encoder->startTag(SYNC_PROVISION_DATA); + if ($policytype == 'MS-WAP-Provisioning-XML') { + // Set 4131 to 0 to require a PIN, 4133 + $this->_encoder->content(''); + } else { + $this->_logger->err('Wrong policy type'); + return false; + } + $this->_encoder->endTag();//data + } + $this->_encoder->endTag();//policy + $this->_encoder->endTag(); //policies + $rwstatus = $this->_driver->getDeviceRWStatus($this->_devId); + + //wipe data if status is pending or wiped + if ($rwstatus == SYNC_PROVISION_RWSTATUS_PENDING || $rwstatus == SYNC_PROVISION_RWSTATUS_WIPED) { + $this->_encoder->startTag(SYNC_PROVISION_REMOTEWIPE, false, true); + $this->_driver->setDeviceRWStatus($this->_devId, SYNC_PROVISION_RWSTATUS_WIPED); + //$rwstatus = SYNC_PROVISION_RWSTATUS_WIPED; + } + + $this->_encoder->endTag();//provision + + return true; + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php new file mode 100644 index 000000000..257e71b87 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php @@ -0,0 +1,529 @@ + + * @package Horde_ActiveSync + */ +/** + * Zarafa Deutschland GmbH, www.zarafaserver.de + * This file is distributed under GPL v2. + * Consult LICENSE file for details + */ +class Horde_ActiveSync_Request_Sync extends Horde_ActiveSync_Request_Base +{ + const STATUS_SUCCESS = 1; + const STATUS_VERSIONMISM = 2; + const STATUS_KEYMISM = 3; + const STATUS_PROTERROR = 4; + const STATUS_SERVERERROR = 5; + + /** + * Handle the sync request + * + * @return boolean + * @throws Horde_ActiveSync_Exception + */ + public function handle(Horde_ActiveSync $activeSync) + { + $this->_logger->info('[Horde_ActiveSync::handleSync] Handling SYNC command.'); + + /* Be optimistic */ + $this->_statusCode = self::STATUS_SUCCESS; + + /* Contains all containers requested */ + $collections = array(); + + /* Start decoding request */ + // FIXME: Need to figure out the proper response structure for errors + // that occur this early + if (!$this->_decoder->getElementStartTag(SYNC_SYNCHRONIZE)) { + throw new Horde_ActiveSync_Exception('Protocol error'); + } + if (!$this->_decoder->getElementStartTag(SYNC_FOLDERS)) { + throw new Horde_ActiveSync_Exception('Protocol error'); + } + + while ($this->_statusCode == self::STATUS_SUCCESS && + $this->_decoder->getElementStartTag(SYNC_FOLDER)) { + + $collection = array(); + $collection['truncation'] = SYNC_TRUNCATION_ALL; + $collection['clientids'] = array(); + $collection['fetchids'] = array(); + + if (!$this->_decoder->getElementStartTag(SYNC_FOLDERTYPE)) { + throw new Horde_ActiveSync_Exception('Protocol error'); + } + + $collection['class'] = $this->_decoder->getElementContent(); + $this->_logger->debug('[Horde_ActiveSync::handleSync] Folder class: ' . $collection['class']); + if (!$this->_decoder->getElementEndTag()) { + throw new Horde_ActiveSync_Exception('Protocol error'); + } + + if (!$this->_decoder->getElementStartTag(SYNC_SYNCKEY)) { + throw new Horde_ActiveSync_Exception('Protocol error'); + } + $collection['synckey'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + throw new Horde_ActiveSync_Exception('Protocol error'); + } + + if ($this->_decoder->getElementStartTag(SYNC_FOLDERID)) { + $collection['id'] = $this->_decoder->getElementContent(); + $this->_logger->debug('[Horde_ActiveSync::handleSync] Folder collectionid: ' . $collection['id']); + if (!$this->_decoder->getElementEndTag()) { + throw new Horde_ActiveSync_Exception('Protocol error'); + } + } + + /* Looks like we ignore the SYNC_SUPPORTED Tag? */ + // @TODO: This needs to be captured and stored in the state so we + // can correctly support ghosted properties + if ($this->_decoder->getElementStartTag(SYNC_SUPPORTED)) { + // SUPPORTED only allowed on initial sync request + if ($collection['synckey'] != 0) { + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleError($collection); + exit; + } + while (1) { + $el = $this->_decoder->getElement(); + if ($el[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) { + break; + } + } + } + + if ($this->_decoder->getElementStartTag(SYNC_DELETESASMOVES)) { + $collection["deletesasmoves"] = true; + } + + if ($this->_decoder->getElementStartTag(SYNC_GETCHANGES)) { + $collection['getchanges'] = true; + } + + if ($this->_decoder->getElementStartTag(SYNC_WINDOWSIZE)) { + $collection['windowsize'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleError($collection); + exit; + } + } + + if ($this->_decoder->getElementStartTag(SYNC_OPTIONS)) { + while(1) { + if ($this->_decoder->getElementStartTag(SYNC_FILTERTYPE)) { + $collection['filtertype'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleError($collection); + exit; + } + } + if ($this->_decoder->getElementStartTag(SYNC_TRUNCATION)) { + $collection['truncation'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleError($collection); + exit; + } + } + if ($this->_decoder->getElementStartTag(SYNC_RTFTRUNCATION)) { + $collection['rtftruncation'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleError($collection); + exit; + } + } + + if ($this->_decoder->getElementStartTag(SYNC_MIMESUPPORT)) { + $collection['mimesupport'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleError($collection); + exit; + } + } + + if ($this->_decoder->getElementStartTag(SYNC_MIMETRUNCATION)) { + $collection['mimetruncation'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleError($collection); + exit; + } + } + + if ($this->_decoder->getElementStartTag(SYNC_CONFLICT)) { + $collection['conflict'] = $this->_decoder->getElementContent(); + if (!$this->_decoder->getElementEndTag()) { + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleError; + exit; + } + } + $e = $this->_decoder->peek(); + if ($e[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) { + $this->_decoder->getElementEndTag(); + break; + } + } + } + + if ($this->_statusCode == self::STATUS_SUCCESS) { + /* Initialize the state */ + $state = &$this->_driver->getStateObject($collection); + try { + $state->loadState($collection['synckey']); + } catch (Horde_ActiveSync_Exception $e) { + $this->_statusCode = self::STATUS_KEYMISM; + $this->_handleError($collection); + exit; + } + + /* compatibility mode - get folderid from the state directory */ + if (!isset($collection['id'])) { + $collection['id'] = $state->getFolderData($this->_devId, $collection['class']); + } + + /* compatibility mode - set default conflict behavior if no + * conflict resolution algorithm is set */ + if (!isset($collection['conflict'])) { + $collection['conflict'] = SYNC_CONFLICT_OVERWRITE_PIM; + } + } + + if ($this->_decoder->getElementStartTag(SYNC_COMMANDS)) { + /* Configure importer with last state */ + $importer = $this->_driver->getImporter(); + $importer->init($state, $collection['id'], $collection['synckey'], $collection['conflict']); + $nchanges = 0; + while (1) { + // MODIFY or REMOVE or ADD or FETCH + $element = $this->_decoder->getElement(); + if ($element[Horde_ActiveSync_Wbxml::EN_TYPE] != Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG) { + $this->_decoder->_ungetElement($element); + break; + } + + $nchanges++; + + if ($this->_decoder->getElementStartTag(SYNC_SERVERENTRYID)) { + $serverid = $this->_decoder->getElementContent(); + + if (!$this->_decoder->getElementEndTag()) {// end serverid + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleError($collection); + exit; + } + } else { + $serverid = false; + } + + if ($this->_decoder->getElementStartTag(SYNC_CLIENTENTRYID)) { + $clientid = $this->_decoder->getElementContent(); + + if (!$this->_decoder->getElementEndTag()) { // end clientid + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleError($collection); + exit; + } + } else { + $clientid = false; + } + + /* Create Streamer object from messages passed from PIM */ + if ($this->_decoder->getElementStartTag(SYNC_DATA)) { + switch ($collection['class']) { + case 'Email': + //@TODO + //$appdata = new SyncMail(); + //$appdata->decode($decoder); + // Remove error code when implemented. + $this->_statusCode = self::STATUS_SERVERERROR; + break; + case 'Contacts': + $appdata = new Horde_ActiveSync_Message_Contact( + array('logger' => $this->_logger, + 'protocolversion' => $this->_version)); + $appdata->decodeStream($this->_decoder); + break; + case 'Calendar': + $appdata = new Horde_ActiveSync_Message_Appointment(array('logger' => $this->_logger)); + $appdata->decodeStream($this->_decoder); + break; + case 'Tasks': + $appdata = new Horde_ActiveSync_Message_Task(array('logger' => $this->_logger)); + $appdata->decodeStream($this->_decoder); + break; + } + if (!$this->_decoder->getElementEndTag()) { + // End application data + $this->_statusCode = self::STATUS_PROTERROR; + break; + } + } + + switch ($element[Horde_ActiveSync_Wbxml::EN_TAG]) { + case SYNC_MODIFY: + if (isset($appdata)) { + // Currently, 'read' is only sent by the PDA when it + // is ONLY setting the read flag. + if (isset($appdata->read)) { + $importer->ImportMessageReadFlag($serverid, $appdata->read); + } else { + $importer->ImportMessageChange($serverid, $appdata); + } + $collection['importedchanges'] = true; + } + break; + case SYNC_ADD: + if (isset($appdata)) { + $id = $importer->ImportMessageChange(false, $appdata); + if ($clientid && $id) { + $collection['clientids'][$clientid] = $id; + $collection['importedchanges'] = true; + } + } + break; + case SYNC_REMOVE: + if (isset($collection['deletesasmoves'])) { + $folderid = $this->_driver->GetWasteBasket(); + + if ($folderid) { + $importer->ImportMessageMove($serverid, $folderid); + $collection['importedchanges'] = true; + break; + } + } + + $importer->ImportMessageDeletion($serverid); + $collection['importedchanges'] = true; + break; + case SYNC_FETCH: + array_push($collection['fetchids'], $serverid); + break; + } + + if (!$this->_decoder->getElementEndTag()) { + // end change/delete/move + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleSyncError($collection); + exit; + } + } + + $this->_logger->debug(sprintf('[Horde_ActiveSync::handleSync] Processed %d incoming changes', $nchanges)); + + if (!$this->_decoder->getElementEndTag()) { + // end commands + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleError($collection); + exit; + } + } + + if (!$this->_decoder->getElementEndTag()) { + // end collection + $this->_statusCode = self::STATUS_PROTERROR; + $this->_handleError($collection); + exit; + } + + array_push($collections, $collection); + } + + if (!$this->_decoder->getElementEndTag()) { + // end collections + return false; + } + + if (!$this->_decoder->getElementEndTag()) { + // end sync + return false; + } + + /* Start output to PIM */ + $this->_encoder->startWBXML(); + $this->_encoder->startTag(SYNC_SYNCHRONIZE); + $this->_encoder->startTag(SYNC_FOLDERS); + foreach ($collections as $collection) { + + /* Get new synckey if needed */ + if (isset($collection['importedchanges']) || + isset($collection['getchanges']) || + $collection['synckey'] == "0") { + + $collection['newsynckey'] = $state->getNewSyncKey($collection['synckey']); + } + + $this->_encoder->startTag(SYNC_FOLDER); + $this->_encoder->startTag(SYNC_FOLDERTYPE); + $this->_encoder->content($collection['class']); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_SYNCKEY); + if (isset($collection['newsynckey'])) { + $this->_encoder->content($collection['newsynckey']); + } else { + $this->_encoder->content($collection['synckey']); + } + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_FOLDERID); + $this->_encoder->content($collection['id']); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_STATUS); + $this->_encoder->content($this->_statusCode); + $this->_encoder->endTag(); + + /* Check the mimesupport because we need it for advanced emails */ + $mimesupport = isset($collection['mimesupport']) ? $collection['mimesupport'] : 0; + + /* Output server IDs for new items we received and added from PIM */ + if (isset($collection['clientids']) || count($collection['fetchids']) > 0) { + $this->_encoder->startTag(SYNC_REPLIES); + foreach ($collection["clientids"] as $clientid => $serverid) { + $this->_encoder->startTag(SYNC_ADD); + $this->_encoder->startTag(SYNC_CLIENTENTRYID); + $this->_encoder->content($clientid); + $this->_encoder->endTag(); + $this->_encoder->startTag(SYNC_SERVERENTRYID); + $this->_encoder->content($serverid); + $this->_encoder->endTag(); + $this->_encoder->startTag(SYNC_STATUS); + $this->_encoder->content(1); + $this->_encoder->endTag(); + $this->_encoder->endTag(); + } + + /* Output any FETCH requests */ + foreach ($collection['fetchids'] as $id) { + $data = $this->_driver->Fetch($collection['id'], $id, $mimesupport); + if ($data !== false) { + $this->_encoder->startTag(SYNC_FETCH); + $this->_encoder->startTag(SYNC_SERVERENTRYID); + $this->_encoder->content($id); + $this->_encoder->endTag(); + $this->_encoder->startTag(SYNC_STATUS); + $this->_encoder->content(1); + $this->_encoder->endTag(); + $this->_encoder->startTag(SYNC_DATA); + $data->encodeStream($this->_encoder); + $this->_encoder->endTag(); + $this->_encoder->endTag(); + } else { + $this->_logger->err(sprintf('[Horde_ActiveSync::handleSync] Unable to fetch %s', $id)); + } + } + $this->_encoder->endTag(); + } + + /* Send server changes to PIM */ + if (isset($collection['getchanges'])) { + $filtertype = isset($collection['filtertype']) ? $collection['filtertype'] : false; + $streamer = new Horde_ActiveSync_Streamer($this->_encoder, $collection['class']); + $exporter = $this->_driver->getExporter(); + $exporter->init($state, $streamer, $collection); + $changecount = $exporter->getChangeCount(); + if (!empty($collection['windowsize']) && $changecount > $collection['windowsize']) { + $this->_encoder->startTag(SYNC_MOREAVAILABLE, false, true); + } + + /* Output message changes per folder */ + $this->_encoder->startTag(SYNC_COMMANDS); + + // Stream the changes to the PDA + $n = 0; + while (1) { + $progress = $exporter->syncronize(); + if (!is_array($progress)) { + break; + } + $n++; + + if (!empty($collection['windowsize']) && $n >= $collection['windowsize']) { + $this->_logger->info(sprintf('[Horde_ActiveSync::handleSync] Exported maxItems of messages: %d - more available.', $collection['windowsize'])); + break; + } + } + $this->_encoder->endTag(); + } + + $this->_encoder->endTag(); + + /* Save the sync state for the next time */ + if (isset($collection['newsynckey'])) { + if (!empty($exporter) || !empty($importer) || !empty($streamer) || $collection['synckey'] == 0) { + $state->setNewSyncKey($collection['newsynckey']); + $state->save(); + } else { + $this->_logger->err(sprintf('[Horde_ActiveSync::handleSync] Error saving %s - no state information available.', $collection["newsynckey"])); + } + } + } + + $this->_encoder->endTag(); + + $this->_encoder->endTag(); + + return true; + } + + /** + * Helper for handling sync errors + * + * @param $collection + */ + private function _handleError($collection) + { + $this->_encoder->startWBXML(); + $this->_encoder->startTag(SYNC_SYNCHRONIZE); + + $this->_encoder->startTag(SYNC_FOLDERS); + + /* Get new synckey if needed */ + if ($this->_statusCode == self::STATUS_KEYMISM || + isset($collection['importedchanges']) || + isset($collection['getchanges']) || + $collection['synckey'] == "0") { + + $collection['newsynckey'] = Horde_ActiveSync_State_Base::getNewSyncKey(($this->_statusCode == self::STATUS_KEYMISM) ? 0 : $collection['synckey']); + // @TODO: Need to reset the state?? + } + + $this->_encoder->startTag(SYNC_FOLDER); + + $this->_encoder->startTag(SYNC_FOLDERTYPE); + $this->_encoder->content($collection['class']); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_SYNCKEY); + if (isset($collection['newsynckey'])) { + $this->_encoder->content($collection['newsynckey']); + } else { + $this->_encoder->content($collection['synckey']); + } + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_FOLDERID); + $this->_encoder->content($collection['id']); + $this->_encoder->endTag(); + + $this->_encoder->startTag(SYNC_STATUS); + $this->_encoder->content($this->_statusCode); + $this->_encoder->endTag(); + + $this->_encoder->endTag(); // SYNC_FOLDER + $this->_encoder->endTag(); + $this->_encoder->endTag(); + } + +} diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/State/Base.php b/framework/ActiveSync/lib/Horde/ActiveSync/State/Base.php new file mode 100644 index 000000000..77b3248bc --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/State/Base.php @@ -0,0 +1,291 @@ + + * @package Horde_ActiveSync + */ +abstract class Horde_ActiveSync_State_Base +{ + + /** + * Filtertype constants + */ + const FILTERTYPE_ALL = 0; + const FILTERTYPE_1DAY = 1; + const FILTERTYPE_3DAYS = 2; + const FILTERTYPE_1WEEK = 3; + const FILTERTYPE_2WEEKS = 4; + const FILTERTYPE_1MONTH = 5; + const FILTERTYPE_3MONTHS = 6; + const FILTERTYPE_6MONTHS = 7; + + /** + * Configuration parameters + * + * @var array + */ + protected $_params; + + /** + * Caches the current state(s) in memory + * + * @var array + */ + protected $_stateCache; + + /** + * The syncKey for the current request. + * + * @var string + */ + protected $_syncKey; + + /** + * The backend driver + * + * @param Horde_ActiveSync_Driver_Base + */ + protected $_backend; + + /** + * The collection array for the collection we are currently syncing. + * Keys include: + * 'class' - The collection class Contacts, Calendar etc... + * 'synckey' - The current synckey + * 'newsynckey' - The new synckey sent back to the PIM + * 'id' - Server folder id + * 'filtertype' - Filter + * 'conflict' - Conflicts + * 'truncation' - Truncation + * + * + * @var array + */ + protected $_collection; + + /** + * Logger instance + * + * @var Horde_Log_Logger + */ + protected $_logger; + + /** + * The PIM device id. Needed for PING requests + * + * @var string + */ + protected $_devId; + + /** + * Const'r + * + * @param array $collection A collection array + * @param array $params All configuration parameters, requirements. + * + * @return Horde_ActiveSync_State_Base + */ + public function __construct($params = array()) + { + $this->_params = $params; + } + + /** + * Update the $oldKey syncState to $newKey. + * + * @param string $newKey + * + * @return void + */ + public function setNewSyncKey($newKey) + { + $this->_syncKey = $newKey; + } + + /** + * Loads the initial state from storage for the specified syncKey and + * intializes the stateMachine for use. + * + * @param string $key The key for the syncState or pingState to load. + * + * @return array The state array + */ + abstract public function loadState($syncKey); + + /** + * Load the ping state for the given device id + * + * @param string $devid The device id. + */ + abstract public function loadPingCollectionState($devid); + + /** + * Get the list of known folders for the specified syncState + * + * @param string $syncKey The syncState key + * + * @return array An array of server folder ids + */ + abstract public function getKnownFolders(); + + /** + * Save the current syncstate to storage + * + * @param string $syncKey + */ + abstract public function save(); + + /** + * Update the state for a specific syncKey + * + * @param $type + * @param $change + * @param $key + */ + abstract public function updateState($type, $change); + + /** + * Obtain the diff between PIM and server + */ + abstract public function getChanges(); + + /** + * Determines if the server version of the message represented by $stat + * conflicts with the PIM version of the message according to the current + * state. + * + * @param array $stat A message stat array + * @param string $type The type of change (change, delete, add) + * + * @return boolean + */ + abstract public function isConflict($stat, $type); + + /** + * Set the backend driver + * (should really only be called by a backend object when passing this + * object to client code) + * + * @param Horde_ActiveSync_Driver_Base $backend The backend driver + * + * @return void + */ + public function setBackend(Horde_ActiveSync_Driver_Base $backend) + { + $this->_backend = $backend; + } + + /** + * Initialize the state object + * + * @param array $collection The collection array + * + * @return void + */ + public function init($collection = array()) + { + $this->_collection = $collection; + } + + /** + * Set the logger instance for this object. + * + * @param Horde_Log_Logger $logger + */ + public function setLogger($logger) + { + $this->_logger = $logger; + } + + /** + * Gets the new sync key for a specified sync key. You must save the new + * sync state under this sync key when done sync'ing by calling + * setNewSyncKey(), then save(). + * + * @param string $syncKey The old syncKey + * + * @return string The new synckey + */ + static public function getNewSyncKey($syncKey) + { + if (empty($syncKey)) { + return '{' . self::uuid() . '}' . '1'; + } else { + if (preg_match('/^s{0,1}\{([a-fA-F0-9-]+)\}([0-9]+)$/', $syncKey, $matches)) { + $n = $matches[2]; + $n++; + + return '{' . $matches[1] . '}' . $n; + } + + // @TODO: should this thrown an exception instead of returning false? + return false; + } + } + + /** + * Generate a uid for the sync key + * + * @return unknown_type + */ + static public function uuid() + { + return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), + mt_rand( 0, 0x0fff ) | 0x4000, + mt_rand( 0, 0x3fff ) | 0x8000, + mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )); + } + + /** + * Returns the timestamp of the earliest modification time to consider + * + * @param integer $restrict The time period to restrict to + * + * @return integer + */ + static protected function _getCutOffDate($restrict) + { + switch($restrict) { + case self::FILTERTYPE_1DAY: + $back = 60 * 60 * 24; + break; + case self::FILTERTYPE_3DAYS: + $back = 60 * 60 * 24 * 3; + break; + case self::FILTERTYPE_1WEEK: + $back = 60 * 60 * 24 * 7; + break; + case self::FILTERTYPE_2WEEKS: + $back = 60 * 60 * 24 * 14; + break; + case self::FILTERTYPE_1MONTH: + $back = 60 * 60 * 24 * 31; + break; + case self::FILTERTYPE_3MONTHS: + $back = 60 * 60 * 24 * 31 * 3; + break; + case self::FILTERTYPE_6MONTHS: + $back = 60 * 60 * 24 * 31 * 6; + break; + default: + break; + } + + if (isset($back)) + { + $date = time() - $back; + return $date; + } else { + return 0; // unlimited + } + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/State/File.php b/framework/ActiveSync/lib/Horde/ActiveSync/State/File.php new file mode 100644 index 000000000..edfb9be1b --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/State/File.php @@ -0,0 +1,623 @@ + + * @package Horde_ActiveSync + */ +/** + * File : statemachine.php + * Project : Z-Push + * Descr : This class handles state requests; + * Each differential mechanism can + * store its own state information, + * which is stored through the + * state machine. SyncKey's are + * of the form {UUID}N, in which + * UUID is allocated during the + * first sync, and N is incremented + * for each request to 'getNewSyncKey'. + * A sync state is simple an opaque + * string value that can differ + * for each backend used - normally + * a list of items as the backend has + * sent them to the PIM. The backend + * can then use this backend + * information to compute the increments + * with current data. + * + * Old sync states are not deleted + * until a sync state is requested. + * At that moment, the PIM is + * apparently requesting an update + * since sync key X, so any sync + * states before X are already on + * the PIM, and can therefore be + * removed. This algorithm is + * automatically enforced by the + * StateMachine class. + * + * + * Created : 01.10.2007 + * + * � Zarafa Deutschland GmbH, www.zarafaserver.de + * This file is distributed under GPL v2. + * Consult LICENSE file for details + */ +class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base +{ + /** + * Directory to store state files + * + * @var stirng + */ + private $_stateDir; + + /** + * Cache for ping state + * + * @var array + */ + private $_pingState; + + /** + * Local cache for changes to *send* to PIM + * (Will remain null until getChanges() is called) + * + * @var + */ + private $_changes; + + /** + * Const'r + * + * @param array $params Must contain 'stateDir' entry + * + * @return Horde_ActiveSync_StateMachine_File + */ + public function __construct($params = array()) + { + parent::__construct($params); + + if (empty($this->_params['stateDir'])) { + throw new InvalidArgumentException('Missing required "stateDir" parameter.'); + } + + $this->_stateDir = $this->_params['stateDir']; + } + + /** + * Load the sync state + * + * @return void + * @throws Horde_ActiveSync_Exception + */ + public function loadState($syncKey) + { + /* Prime the state cache for the first sync */ + if (empty($syncKey)) { + $this->_stateCache = array(); + return; + } + + // Check if synckey is allowed + if (!preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $syncKey, $matches)) { + throw new Horde_ActiveSync_Exception('Invalid sync key'); + } + + // Cleanup all older syncstates + $this->_gc($syncKey); + + // Read current sync state + $filename = $this->_stateDir . '/' . $syncKey; + if (!file_exists($filename)) { + throw new Horde_ActiveSync_Exception('Sync state not found'); + } + $this->_stateCache = unserialize(file_get_contents($filename)); + + $this->_syncKey = $syncKey; + } + + /** + * Determines if the server version of the message represented by $stat + * conflicts with the PIM version of the message according to the current + * state. + * + * @see Horde_ActiveSync_State_Base::isConflict() + */ + public function isConflict($stat, $type) + { + foreach ($this->_stateCache as $state) { + if ($state['id'] == $stat['id']) { + $oldstat = $state; + break; + } + } + + // New message on server - can never conflict, but we shouldn't be + // here in this case anyway, right? + if (!isset($oldstat)) { + return false; + } + + if ($state['mod'] != $oldstat['mod']) { + // Changed here + if ($type == 'delete' || $type == 'change') { + // changed here, but deleted there -> conflict, + // or changed here and changed there -> conflict + return true; + } else { + // changed here, and other remote changes (move or flags) + return false; + } + } + } + + /** + * Save the current state to storage + * + * @param string $syncKey The sync key to save + * + * @return boolean + */ + public function save() + { + return file_put_contents($this->_stateDir . '/' . $this->_syncKey, !empty($this->_stateCache) ? serialize($this->_stateCache) : ''); + } + + /** + * Update the state to reflect changes + * + * @param string $type The type of change (change, delete, flags) + * @param array $change Array describing change + * + * @return void + */ + public function updateState($type, $change) + { + if (empty($this->_stateCache)) { + $this->_stateCache = array(); + } + + // Change can be a change or an add + if ($type == 'change') { + for($i = 0; $i < count($this->_stateCache); $i++) { + if($this->_stateCache[$i]['id'] == $change['id']) { + $this->_stateCache[$i] = $change; + /* If we have a pingState, keep it in sync */ + if (!empty($this->_pingState['collections'])) { + $this->_pingState['collections'][$this->_collection['class']]['state'] = $this->_stateCache; + } + return; + } + } + // Not found, add as new + $this->_stateCache[] = $change; + } else { + for ($i = 0; $i < count($this->_stateCache); $i++) { + // Search for the entry for this item + if ($this->_stateCache[$i]['id'] == $change['id']) { + if ($type == 'flags') { + // Update flags + $this->_stateCache[$i]['flags'] = $change['flags']; + } elseif ($type == 'delete') { + // Delete item + array_splice($this->_stateCache, $i, 1); + } + break; + } + } + } + + /* If we have a pingState, keep it in sync */ + if (!empty($this->_pingState['collections'])) { + $this->_pingState['collections'][$this->_collection['class']]['state'] = $this->_stateCache; + } + } + + /** + * Save folder data for a specific device + * + * @param string $devId The device Id + * @param array $folders The folder data + * + * @return boolean + * @throws Horde_ActiveSync_Exception + */ + public function setFolderData($devId, $folders) + { + if (!is_array($folders) || empty ($folders)) { + return false; + } + + $unique_folders = array (); + foreach ($folders as $folder) { + // don't save folder-ids for emails + if ($folder->type == SYNC_FOLDER_TYPE_INBOX) { + continue; + } + + // no folder from that type or the default folder + if (!array_key_exists($folder->type, $unique_folders) || $folder->parentid == 0) { + $unique_folders[$folder->type] = $folder->serverid; + } + } + + // Treo does initial sync for calendar and contacts too, so we need to fake + // these folders if they are not supported by the backend + if (!array_key_exists(SYNC_FOLDER_TYPE_APPOINTMENT, $unique_folders)) { + $unique_folders[SYNC_FOLDER_TYPE_APPOINTMENT] = SYNC_FOLDER_TYPE_DUMMY; + } + if (!array_key_exists(SYNC_FOLDER_TYPE_CONTACT, $unique_folders)) { + $unique_folders[SYNC_FOLDER_TYPE_CONTACT] = SYNC_FOLDER_TYPE_DUMMY; + + } + if (!file_put_contents($this->_stateDir . '/compat-' . $devId, serialize($unique_folders))) { + $this->logError('_saveFolderData: Data could not be saved!'); + throw new Horde_ActiveSync_Exception('Folder data could not be saved'); + } + } + + /** + * Get the folder data for a specific device + * + * @param string $devId The device id + * @param string $class The folder class to fetch (Calendar, Contacts etc.) + * + * @return mixed Either an array of folder data || false + */ + public function getFolderData($devId, $class) + { + $filename = $this->_stateDir . '/compat-' . $devId; + if (file_exists($filename)) { + $arr = unserialize(file_get_contents($filename)); + if ($class == "Calendar") { + return $arr[SYNC_FOLDER_TYPE_APPOINTMENT]; + } + if ($class == "Contacts") { + return $arr[SYNC_FOLDER_TYPE_CONTACT]; + } + } + + return false; + } + + /** + * Return an array of known folders. + * + * @return array + */ + public function getKnownFolders() + { + if (!isset($this->_stateCache)) { + throw new Horde_ActiveSync_Exception('Sync state not loaded'); + } + $folders = array(); + foreach ($this->_stateCache as $folder) { + $folders[] = $folder['id']; + } + return $folders; + } + + /** + * Perform any initialization needed to deal with pingStates + * For this driver, it loads the device's state file. + * + * @param string $devId The device id of the PIM to load PING state for + * + * @return The $collection array + */ + public function initPingState($devId) + { + $this->_devId = $devId; + $file = $this->_stateDir . '/' . $devId; + if (file_exists($file)) { + $this->_pingState = unserialize(file_get_contents($file)); + } else { + $this->_pingState = array( + 'lifetime' => 0, + 'collections' => array()); + } + + return $this->_pingState['collections']; + } + + /** + * Load a specific collection's ping state + * + * @param array $pingCollection The collection array from the PIM request + * + * @return void + * @throws Horde_ActiveSync_Exception + */ + public function loadPingCollectionState($pingCollection) + { + if (empty($this->_pingState)) { + throw new Horde_ActiveSync_Exception('PING state not initialized'); + } + + $haveState = false; + + /* Load any existing state */ + // @TODO: I'm almost positive we need to key these by 'id', not 'class' + // but this is what z-push did so... + if (!empty($this->_pingState['collections'][$pingCollection['class']])) { + $this->_collection = $this->_pingState['collections'][$pingCollection['class']]; + $this->_collection['synckey'] = $this->_devId; + $this->_stateCache = $this->_collection['state']; + $haveState = true; + } + + /* Initialize state for this collection */ + if (!$haveState) { + $this->_logger->debug('Empty state for '. $pingCollection['class']); + + /* Start with empty state cache */ + //$this->_stateCache[$pingCollection['id']] = array(); + + /* Init members for the getChanges call */ + $this->_syncKey = $this->_devId; + $this->_collection = $pingCollection; + $this->_collection['synckey'] = $this->_devId; + $this->_collection['state'] = array(); + + /* If we are here, then the pingstate was empty, prime it */ + $this->_pingState['collections'][$this->_collection['class']] = $this->_collection; + + /* Need to load _stateCache so getChanges has it */ + $this->_stateCache = array(); + + $changes = $this->getChanges(); + foreach ($changes as $change) { + switch ($change['type']) { + case 'change': + $stat = $this->_backend->StatMessage($this->_collection['id'], $change['id']); + if (!$message = $this->_backend->GetMessage($this->_collection['id'], $change['id'], 0)) { + throw new Horde_ActiveSync_Exception('Message not found'); + } + if ($stat && $message) { + $this->updateState('change', $stat); + } + break; + + default: + throw new Horde_ActiveSync_Exception('Unexpected change type in loadPingState'); + } + } + + $this->_pingState['collections'][$this->_collection['class']]['state'] = $this->_stateCache; + $this->savePingState(); + } + } + + /** + * Save the current ping state to storage + * + * @param string $devId The PIM device id + * @param integer $lifetime The ping heartbeat/lifetime interval + * + * @return boolean + * @throws Horde_ActiveSync_Exception + */ + public function savePingState() + { + if (empty($this->_pingState)) { + throw new Horde_ActiveSync_Exception('PING state not initialized'); + } + $state = serialize(array('lifetime' => $this->_pingState['lifetime'], 'collections' => $this->_pingState['collections'])); + + return file_put_contents($this->_stateDir . '/' . $this->_devId, $state); + } + + /** + * Return the heartbeat interval, or zero if we have no existing state + * + * @param string $devId + * + * @return integer The hearbeat interval, or zero if not found. + * @throws Horde_ActiveSync_Exception + */ + public function getPingLifetime() + { + if (empty($this->_pingState)) { + throw new Horde_ActiveSync_Exception('PING state not initialized'); + } + + return (!$this->_pingState) ? 0 : $this->_pingState['lifetime']; + } + + public function setPingLifetime($lifetime) + { + $this->_pingState['lifetime'] = $lifetime; + } + + /** + * + * @param $collectionClass The collection we are syncing + * + * @return + */ + public function getChanges($flags = 0) + { + $syncState = empty($this->_stateCache) ? array() : $this->_stateCache; + $cutoffdate = self::_getCutOffDate(!empty($this->_collection['filtertype']) ? $this->_collection['filtertype'] : 0); + + if (!empty($this->_collection['id'])) { + $folderId = $this->_collection['id']; + $this->_logger->debug('Initializing message diff engine'); + if (!$syncState) { + $syncState = array(); + } + $this->_logger->debug(count($syncState) . ' messages in state'); + + //do nothing if it is a dummy folder + if ($folderId != SYNC_FOLDER_TYPE_DUMMY) { + // on ping: check if backend supports alternative PING mechanism & use it + if ($this->_collection['class'] === false && $flags == BACKEND_DISCARD_DATA && $this->_backend->AlterPing()) { + //@TODO - look at the passing of syncstate here - should probably pass self?? + // Not even sure if we need this AlterPing? + $this->_changes = $this->_backend->AlterPingChanges($folderId, $syncState); + } else { + // Get our lists - syncstate (old) and msglist (new) + $msglist = $this->_backend->GetMessageList($this->_collection['id'], $cutoffdate); + if ($msglist === false) { + return false; + } + $this->_changes = $this->_getDiff($syncState, $msglist); + } + } + $this->_logger->debug('Found ' . count($this->_changes) . ' message changes'); + + } else { + + $this->_logger->debug('Initializing folder diff engine'); + $folderlist = $this->_backend->getFolderList(); + if ($folderlist === false) { + return false; + } + + $this->_changes = $this->_getDiff($syncState, $folderlist); + $this->_logger->debug('Config: Found ' . count($this->_changes) . ' folder changes'); + } + + return $this->_changes; + } + + public function getChangeCount() + { + if (!isset($this->_changes)) { + $this->getChanges(); + //throw new Horde_ActiveSync_Exception('Changes not yet retrieved. Must call getChanges() first'); + } + return count($this->_changes); + } + + /** + * Garbage collector - clean up from previous sync + * requests. + * + * @params string $syncKey The sync key + * + * @throws Horde_ActiveSync_Exception + * @return boolean? + */ + protected function _gc($syncKey) + { + if (!preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $syncKey, $matches)) { + return false; + } + $guid = $matches[1]; + $n = $matches[2]; + + $dir = opendir($this->_stateDir); + if (!$dir) { + return false; + } + while ($entry = readdir($dir)) { + if (preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $entry, $matches)) { + if ($matches[1] == $guid && $matches[2] < $n) { + unlink($this->_stateDir . '/' . $entry); + } + } + } + + return true; + } + + /** + * + * @param $old + * @param $new + * @return unknown_type + */ + private function _getDiff($old, $new) + { + $changes = array(); + + // Sort both arrays in the same way by ID + usort($old, array(__CLASS__, 'RowCmp')); + usort($new, array(__CLASS__, 'RowCmp')); + + $inew = 0; + $iold = 0; + + // Get changes by comparing our list of messages with + // our previous state + while (1) { + $change = array(); + + if ($iold >= count($old) || $inew >= count($new)) { + break; + } + + if ($old[$iold]['id'] == $new[$inew]['id']) { + // Both messages are still available, compare flags and mod + if (isset($old[$iold]['flags']) && isset($new[$inew]['flags']) && $old[$iold]['flags'] != $new[$inew]['flags']) { + // Flags changed + $change['type'] = 'flags'; + $change['id'] = $new[$inew]['id']; + $change['flags'] = $new[$inew]['flags']; + $changes[] = $change; + } + + if ($old[$iold]['mod'] != $new[$inew]['mod']) { + $change['type'] = 'change'; + $change['id'] = $new[$inew]['id']; + $changes[] = $change; + } + + $inew++; + $iold++; + } else { + if ($old[$iold]['id'] > $new[$inew]['id']) { + // Message in state seems to have disappeared (delete) + $change['type'] = 'delete'; + $change['id'] = $old[$iold]['id']; + $changes[] = $change; + $iold++; + } else { + // Message in new seems to be new (add) + $change['type'] = 'change'; + $change['flags'] = SYNC_NEWMESSAGE; + $change['id'] = $new[$inew]['id']; + $changes[] = $change; + $inew++; + } + } + } + + while ($iold < count($old)) { + // All data left in _syncstate have been deleted + $change['type'] = 'delete'; + $change['id'] = $old[$iold]['id']; + $changes[] = $change; + $iold++; + } + + while ($inew < count($new)) { + // All data left in new have been added + $change['type'] = 'change'; + $change['flags'] = SYNC_NEWMESSAGE; + $change['id'] = $new[$inew]['id']; + $changes[] = $change; + $inew++; + } + + return $changes; + } + + /** + * + * @param $a + * @param $b + * @return unknown_type + */ + static public function RowCmp($a, $b) + { + return $a['id'] < $b['id'] ? 1 : -1; + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/State/History.php b/framework/ActiveSync/lib/Horde/ActiveSync/State/History.php new file mode 100644 index 000000000..a50e8ab2b --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/State/History.php @@ -0,0 +1,595 @@ + + * @package Horde_ActiveSync + */ +class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base +{ + /** + * Cache for ping state + * + * @var array + */ + private $_pingState; + + /** + * The timestamp for the last syncKey + * + * @var timestamp + */ + private $_lastSyncTS; + + /** + * The current sync timestamp + * + * @var timestamp + */ + private $_thisSyncTS; + + + /** + * Local cache of changes that need to be sent + * + * @var array + */ + private $_changes; + + /** + * DB handle + * + * @var Horde_Db_Adapter_Base + */ + protected $_db; + + /** + * Const'r + * + * @param array $params Must contain: + * 'db' - Horde_Db + * 'syncStateTable' - Name of table for storing syncstate + * 'pingTable' - Name of table for storing ping data + * 'syncChangesTable' - Name of table for remembering what changes + * are due to PIM import so we don't mirror the + * changes back to the PIM on next Sync + * + * @return Horde_ActiveSync_StateMachine_File + */ + public function __construct($params = array()) + { + parent::__construct($params); + + if (empty($this->_params['db']) || !($this->_params['db'] instanceof Horde_Db_Adapter_Base)) { + throw new InvalidArgumentException('Missing or invalid Horde_Db parameter.'); + } + $this->_params = $params['db']; + } + + /** + * Load the sync state + * + * @return void + * @throws Horde_ActiveSync_Exception + */ + public function loadState($syncKey) + { + if (empty($syncKey)) { + return; + } + + // Check if synckey is allowed + if (!preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $syncKey, $matches)) { + throw new Horde_ActiveSync_Exception('Invalid sync key'); + } + $this->_syncKey = $syncKey; + + try { + $results = $this->_db->selectOne('SELECT sync_data, sync_devId, sync_time FROM ' . $this->_syncStateTable . ' WHERE sync_key = ?', array($this->_syncKey)); + } catch (Horde_Db_Exception $e) { + throw new Horde_ActiveSync_Exception($e); + } + + /* Load the previous syncState from storage */ + $this->_lastSyncTS = $results['sync_time']; + $this->_devId = $results['sync_devId']; + $this->_changes = unserialize(sync_data); + } + + /** + * Determines if the server version of the message represented by $stat + * conflicts with the PIM version of the message. For this driver, this is + * true whenever $lastSyncTime is older then $stat['mod']. Method is only + * called from the Importer during an import of a non-new change from the + * PIM. + * + * @see Horde_ActiveSync_State_Base::isConflict() + */ + public function isConflict($stat, $type) + { + // $stat == server's message information + if ($stat['mod'] > $this->_lastSyncTS) { + if ($type == 'delete' || $type == 'change') { + // changed here - deleted there + // changed here - changed there + return true; + } else { + // all other remote cahnges are fine (move/flags) + return false; + } + } + } + + /** + * Save the current state to storage + * + * @return boolean + * @throws Horde_ActiveSync_Exception + */ + public function save() + { + // Update state table to remember this last synctime and key + $sql = 'INSERT INTO ' . $this->_syncStateTable . ' (sync_key, sync_data, sync_devId, sync_time) VALUES (?, ?, ?, ?)'; + + /* Remember any left over changes */ + $data = (isset($this->_changes) ? serialize($this->_changes) : serialize(array())); + + try { + $this->_db->insert($sql, array($this->_syncKey, $data, $this->_devId, $this->_thisSyncTS)); + } catch (Horde_Db_Exception $e) { + throw new Horde_ActiveSync_Exception($e); + } + + return true; + } + + /** + * Update the state to reflect changes + * + * Notes: Since PIM changes are dealt with before Server changes, we can + * use a null $_changes array to detect what we are updating for. If we + * are importing PIM changes, need to update the syncChangesTable so we + * don't mirror back the changes on next sync. If we are exporting server + * changes, we need to track which changes have been sent (by removing them + * from _changes) so we know which items to send on the next sync if a + * MOREAVAILBLE response was needed. + * + * @param string $type The type of change (change, delete, flags) + * @param array $change Array describing change + * + * @return void + */ + public function updateState($type, $change) + { + if (!isset($this->_changes)) { + /* We must be updating state during receiving changes from PIM */ + $sql = 'INSERT INTO ' . $this->_syncChangesTable . ' (message_uid, sync_mod_time, sync_key) VALUES (?, ?, ?)'; + try { + $this->_db->insert($sql, array($change['id'], time(), $this->_syncKey)); + } catch (Horde_Db_Exception $e) { + throw new Horde_ActiveSync_Exception($e); + } + } else { + /* When sending server changes, $this->_changes will contain all + * changes. Need to track which ones are sent since we might not + * send all of them. + */ + for ($i = 0; $i < count($this->_changes); $i++) { + if ($this->_changes[$i]['id'] == $change['id']) { + unset($this->_changes[$i]); + } + } + } + } + + /** + * Save folder data for a specific device + * + * @param string $devId The device Id + * @param array $folders The folder data + * + * @return boolean + * @throws Horde_ActiveSync_Exception + */ + public function setFolderData($devId, $folders) + { + if (!is_array($folders) || empty ($folders)) { + return false; + } + + $unique_folders = array (); + foreach ($folders as $folder) { + // don't save folder-ids for emails + if ($folder->type == SYNC_FOLDER_TYPE_INBOX) { + continue; + } + + // no folder from that type or the default folder + if (!array_key_exists($folder->type, $unique_folders) || $folder->parentid == 0) { + $unique_folders[$folder->type] = $folder->serverid; + } + } + + // Treo does initial sync for calendar and contacts too, so we need to fake + // these folders if they are not supported by the backend + if (!array_key_exists(SYNC_FOLDER_TYPE_APPOINTMENT, $unique_folders)) { + $unique_folders[SYNC_FOLDER_TYPE_APPOINTMENT] = SYNC_FOLDER_TYPE_DUMMY; + } + if (!array_key_exists(SYNC_FOLDER_TYPE_CONTACT, $unique_folders)) { + $unique_folders[SYNC_FOLDER_TYPE_CONTACT] = SYNC_FOLDER_TYPE_DUMMY; + } + /* Storage to SQL? */ +// +// if (!file_put_contents($this->_stateDir . '/compat-' . $devId, serialize($unique_folders))) { +// $this->logError('_saveFolderData: Data could not be saved!'); +// throw new Horde_ActiveSync_Exception('Folder data could not be saved'); +// } + } + + /** + * Get the folder data for a specific device + * + * @param string $devId The device id + * @param string $class The folder class to fetch (Calendar, Contacts etc.) + * + * @return mixed Either an array of folder data || false + */ + public function getFolderData($devId, $class) + { +// $filename = $this->_stateDir . '/compat-' . $devId; +// if (file_exists($filename)) { +// $arr = unserialize(file_get_contents($filename)); +// if ($class == "Calendar") { +// return $arr[SYNC_FOLDER_TYPE_APPOINTMENT]; +// } +// if ($class == "Contacts") { +// return $arr[SYNC_FOLDER_TYPE_CONTACT]; +// } +// } +// +// return false; + } + + public function getKnownFolders($syncKey) + { + + $sql = 'SELECT state_data from ' . $this->_table . ' WHERE state_syncKey = ?'; + // + // + + } + + public function setKnownFolders($syncKey, $folders) + { + $sql = 'INSERT INTO ' . $this->_table . '....'; + + // Need to GC the table, delete all but the *two* most recent synckeys + // for this devId. Need the latest one, but also the previous one in + // case the device did not correctly receive the response - it will + // continue to send the previous syncKey, so we need to remember the + // state. + + } + + /** + * Perform any initialization needed to deal with pingStates + * For this driver, it loads the device's state file. + * + * @param string $devId The device id of the PIM to load PING state for + * + * @return The $collection array + */ + public function initPingState($devId) + { + $this->_devId = $devId; + + $sql = 'SELECT ping_state FROM ' . $this->_pingTable . ' WHERE ping_devid = ?'; + + $this->_pingState = unserialize($results); + // Try to get pingstate from SQL (need lifetime and last synctime) + //$this->_pingState = unserialize($sqlResults); + + // If no existing state - initialize + // $this->_pingState = array( + // 'lifetime' => 0, + // 'collections' => array()); + + return $this->_pingState['collections']; + } + + /** + * Load a specific collection's ping state + * + * @param array $pingCollection The collection array from the PIM request + * + * @return void + * @throws Horde_ActiveSync_Exception + */ + public function loadCollectionPingState($pingCollection) + { + if (empty($this->_pingState)) { + throw new Horde_ActiveSync_Exception('PING state not initialized'); + } + + $haveState = false; + + /* Load any existing state */ + // @TODO: I'm almost positive we need to key these by 'id', not 'class' + // but this is what z-push did so... + if (!empty($this->_pingState['collections'][$pingCollection['class']])) { + $this->_collection = $this->_pingState['collections'][$pingCollection['class']]; + $this->_collection['synckey'] = $this->_devId; + //$this->_stateCache = $this->_collection['state']; + $haveState = true; + } + + /* Initialize state for this collection */ + if (!$haveState) { + $this->_logger->debug('Empty state for '. $pingCollection['class']); + + /* Start with empty state cache */ + //$this->_stateCache[$pingCollection['id']] = array(); + + /* Init members for the getChanges call */ + $this->_syncKey = $this->_devId; + $this->_collection = $pingCollection; + $this->_collection['synckey'] = $this->_devId; + $this->_collection['state'] = array(); + + /* If we are here, then the pingstate was empty, prime it */ + $this->_pingState['collections'][$this->_collection['class']] = $this->_collection; + + /* Need to load _stateCache so getChanges has it */ + $this->_stateCache = array(); + + $changes = $this->getChanges(); + foreach ($changes as $change) { + switch ($change['type']) { + case 'change': + $stat = $this->_backend->StatMessage($this->_collection['id'], $change['id']); + if (!$message = $this->_backend->GetMessage($this->_collection['id'], $change['id'], 0)) { + throw new Horde_ActiveSync_Exception('Message not found'); + } + if ($stat && $message) { + $this->updateState('change', $stat); + } + break; + + default: + throw new Horde_ActiveSync_Exception('Unexpected change type in loadPingState'); + } + } + + $this->_pingState['collections'][$this->_collection['class']]['state'] = $this->_stateCache; + $this->savePingState(); + } + } + + /** + * Save the current ping state to storage + * + * @param string $devId The PIM device id + * @param integer $lifetime The ping heartbeat/lifetime interval + * + * @return boolean + * @throws Horde_ActiveSync_Exception + */ + public function savePingState() + { + if (empty($this->_pingState)) { + throw new Horde_ActiveSync_Exception('PING state not initialized'); + } + $state = serialize(array('lifetime' => $this->_pingState['lifetime'], 'collections' => $this->_pingState['collections'])); + + // Need to write to DB + return ;//file_put_contents($this->_stateDir . '/' . $this->_devId, $state); + } + + /** + * Return the heartbeat interval, or zero if we have no existing state + * + * @param string $devId + * + * @return integer The hearbeat interval, or zero if not found. + * @throws Horde_ActiveSync_Exception + */ + public function getPingLifetime() + { + if (empty($this->_pingState)) { + throw new Horde_ActiveSync_Exception('PING state not initialized'); + } + + return (!$this->_pingState) ? 0 : $this->_pingState['lifetime']; + } + + public function setPingLifetime($lifetime) + { + $this->_pingState['lifetime'] = $lifetime; + } + + /** + * Get all items that have changed since the last sync time + * + * @param integer $flags + * + * @return array + */ + public function getChanges($flags = 0) + { + $cutoffdate = self::_getCutOffDate(!empty($this->_collection['filtertype']) ? $this->_collection['filtertype'] : 0); + + if (!empty($this->_collection['id'])) { + $folderId = $this->_collection['id']; + $this->_logger->debug('Initializing message diff engine'); + + //do nothing if it is a dummy folder + if ($folderId != SYNC_FOLDER_TYPE_DUMMY) { + /* First, need to see if we have exising changes left over + * from a previous sync that resulted in a MORE_AVAILABLE */ + if (!$empty($this->_changes)) { + return $this->_changes; + } + + /* No existing changes, poll the backend */ + $this->_thisSyncTS = time(); + $this->_changes = $this->_backend->getServerChanges($folderId, $this->_lastSyncTS, $this->_thisSyncTS); + } + $this->_logger->debug('Found ' . count($this->_changes) . ' message changes'); + + } else { + + $this->_logger->debug('Initializing folder diff engine'); + $this->_thisSyncTS = time(); + $folderlist = $this->_backend->getFolderList(); + if ($folderlist === false) { + return false; + } + + if (!isset($syncState) || !$syncState) { + $syncState = array(); + } + + $this->_changes = $this->_getDiff($syncState, $folderlist); + $this->_logger->debug('Config: Found ' . count($this->_changes) . ' folder changes'); + } + + return $this->_changes; + } + + public function getChangeCount() + { + if (!isset($this->_changes)) { + $this->getChanges(); + //throw new Horde_ActiveSync_Exception('Changes not yet retrieved. Must call getChanges() first'); + } + return count($this->_changes); + } + + /** + * Garbage collector - clean up from previous sync + * requests. + * + * @params string $syncKey The sync key + * + * @throws Horde_ActiveSync_Exception + * @return boolean? + */ + protected function _gc($syncKey) + { + if (!preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $syncKey, $matches)) { + return false; + } + $guid = $matches[1]; + $n = $matches[2]; + + $dir = opendir($this->_stateDir); + if (!$dir) { + return false; + } + while ($entry = readdir($dir)) { + if (preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $entry, $matches)) { + if ($matches[1] == $guid && $matches[2] < $n) { + unlink($this->_stateDir . '/' . $entry); + } + } + } + + return true; + } + + /** + * + * @param $old + * @param $new + * @return unknown_type + */ + private function _getDiff($old, $new) + { + $changes = array(); + + // Sort both arrays in the same way by ID + usort($old, array(__CLASS__, 'RowCmp')); + usort($new, array(__CLASS__, 'RowCmp')); + + $inew = 0; + $iold = 0; + + // Get changes by comparing our list of messages with + // our previous state + while (1) { + $change = array(); + + if ($iold >= count($old) || $inew >= count($new)) { + break; + } + + if ($old[$iold]['id'] == $new[$inew]['id']) { + // Both messages are still available, compare flags and mod + if (isset($old[$iold]['flags']) && isset($new[$inew]['flags']) && $old[$iold]['flags'] != $new[$inew]['flags']) { + // Flags changed + $change['type'] = 'flags'; + $change['id'] = $new[$inew]['id']; + $change['flags'] = $new[$inew]['flags']; + $changes[] = $change; + } + + if ($old[$iold]['mod'] != $new[$inew]['mod']) { + $change['type'] = 'change'; + $change['id'] = $new[$inew]['id']; + $changes[] = $change; + } + + $inew++; + $iold++; + } else { + if ($old[$iold]['id'] > $new[$inew]['id']) { + // Message in state seems to have disappeared (delete) + $change['type'] = 'delete'; + $change['id'] = $old[$iold]['id']; + $changes[] = $change; + $iold++; + } else { + // Message in new seems to be new (add) + $change['type'] = 'change'; + $change['flags'] = SYNC_NEWMESSAGE; + $change['id'] = $new[$inew]['id']; + $changes[] = $change; + $inew++; + } + } + } + + while ($iold < count($old)) { + // All data left in _syncstate have been deleted + $change['type'] = 'delete'; + $change['id'] = $old[$iold]['id']; + $changes[] = $change; + $iold++; + } + + while ($inew < count($new)) { + // All data left in new have been added + $change['type'] = 'change'; + $change['flags'] = SYNC_NEWMESSAGE; + $change['id'] = $new[$inew]['id']; + $changes[] = $change; + $inew++; + } + + return $changes; + } + + /** + * + * @param $a + * @param $b + * @return unknown_type + */ + static public function RowCmp($a, $b) + { + return $a['id'] < $b['id'] ? 1 : -1; + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Streamer.php b/framework/ActiveSync/lib/Horde/ActiveSync/Streamer.php new file mode 100644 index 000000000..19f3cca3e --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Streamer.php @@ -0,0 +1,124 @@ +_encoder = &$encoder; + $this->_type = $class; + $this->_seenObjects = array(); + } + + /** + * + * @param $id + * @param $message + * @return unknown_type + */ + public function messageChange($id, $message) + { + if ($message->getClass() != $this->_type) { + return true; // ignore other types + } + + // prevent sending the same object twice in one request + if (in_array($id, $this->_seenObjects)) { + return true; + } + + $this->_seenObjects[] = $id; + if ($message->flags === false || $message->flags === SYNC_NEWMESSAGE) { + $this->_encoder->startTag(SYNC_ADD); + } else { + $this->_encoder->startTag(SYNC_MODIFY); + } + + $this->_encoder->startTag(SYNC_SERVERENTRYID); + $this->_encoder->content($id); + $this->_encoder->endTag(); + $this->_encoder->startTag(SYNC_DATA); + $message->encodeStream($this->_encoder); + $this->_encoder->endTag(); + $this->_encoder->endTag(); + + return true; + } + + /** + * + * @param $id + * @return unknown_type + */ + public function messageDeletion($id) + { + $this->_encoder->startTag(SYNC_REMOVE); + $this->_encoder->startTag(SYNC_SERVERENTRYID); + $this->_encoder->content($id); + $this->_encoder->endTag(); + $this->_encoder->endTag(); + + return true; + } + + /** + * + * @param $id + * @param $flags + * @return unknown_type + */ + public function messageReadFlag($id, $flags) + { + if ($this->_type != "syncmail") { + return true; + } + $this->_encoder->startTag(SYNC_MODIFY); + $this->_encoder->startTag(SYNC_SERVERENTRYID); + $this->_encoder->content($id); + $this->_encoder->endTag(); + $this->_encoder->startTag(SYNC_DATA); + $this->_encoder->startTag(SYNC_POOMMAIL_READ); + $this->_encoder->content($flags); + $this->_encoder->endTag(); + $this->_encoder->endTag(); + $this->_encoder->endTag(); + + return true; + } + + /** + * + * @param $message + * @return unknown_type + */ + function messageMove($message) + { + return true; + } +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Timezone.php b/framework/ActiveSync/lib/Horde/ActiveSync/Timezone.php new file mode 100644 index 000000000..589dfba0a --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Timezone.php @@ -0,0 +1,199 @@ + + * + * @category Horde + * @package Horde_Rpc + */ +class Horde_ActiveSync_Timezone +{ + + /** + * Convert a timezone from the ActiveSync base64 structure to a TZ offset + * hash. + * + * @param base64 encoded timezone structure defined by MS as: + *
+     *      typedef struct TIME_ZONE_INFORMATION {
+     *        LONG Bias;
+     *        WCHAR StandardName[32];
+     *        SYSTEMTIME StandardDate;
+     *        LONG StandardBias;
+     *        WCHAR DaylightName[32];
+     *        SYSTEMTIME DaylightDate;
+     *        LONG DaylightBias;};
+     *  
+ * + * With the SYSTEMTIME format being: + *
+     * typedef struct _SYSTEMTIME {
+     *     WORD wYear;
+     *     WORD wMonth;
+     *     WORD wDayOfWeek;
+     *     WORD wDay;
+     *     WORD wHour;
+     *     WORD wMinute;
+     *     WORD wSecond;
+     *     WORD wMilliseconds;
+     *   } SYSTEMTIME, *PSYSTEMTIME;
+     *  
+ * + * See: http://msdn.microsoft.com/en-us/library/ms724950%28VS.85%29.aspx + * and: http://msdn.microsoft.com/en-us/library/ms725481%28VS.85%29.aspx + * + * @return array Hash of offset information + */ + static public function getOffsetsFromSyncTZ($data) + { + $tz = unpack('lbias/a64stdname/vstdyear/vstdmonth/vstdday/vstdweek/vstdhour/vstdminute/vstdsecond/vstdmillis/' . + 'lstdbias/a64dstname/vdstyear/vdstmonth/vdstday/vdstweek/vdsthour/vdstminute/vdstsecond/vdstmillis/' . + 'ldstbias', base64_decode($data)); + $tz['timezone'] = $tz['bias']; + $tz['timezonedst'] = $tz['dstbias']; + + return $tz; + } + + + /** + * Build an ActiveSync TZ blob given a TZ Offset hash. + * + * @param array $offsets A TZ offset hash + * + * @return string A base64_encoded ActiveSync Timezone structure suitable + * for transmitting via wbxml. + */ + static public function getSyncTZFromOffsets($offsets) + { + $packed = pack('la64vvvvvvvvla64vvvvvvvvl', + $offsets['bias'], '', 0, $offsets['stdmonth'], $offsets['stdday'], $offsets['stdweek'], $offsets['stdhour'], $offsets['stdminute'], $offsets['stdsecond'], $offsets['stdmillis'], + $offsets['stdbias'], '', 0, $offsets['dstmonth'], $offsets['dstday'], $offsets['dstweek'], $offsets['dsthour'], $offsets['dstminute'], $offsets['dstsecond'], $offsets['dstmillis'], + $offsets['dstbias']); + + return base64_encode($packed); + } + + /** + * Create a offset hash suitable for use in ActiveSync transactions + * + * @param Horde_Date $date A date object representing the date to base the + * the tz data on. + */ + static public function getOffsetsFromDate($date) + { + $offsets = array( + 'bias' => 0, + 'stdname' => '', + 'stdyear' => 0, + 'stdmonth' => 0, + 'stdday' => 0, + 'stdweek' => 0, + 'stdhour' => 0, + 'stdminute' => 0, + 'stdsecond' => 0, + 'stdmillis' => 0, + 'stdbias' => 0, + 'dstname' => '', + 'dstyear' => 0, + 'dstmonth' => 0, + 'dstday' => 0, + 'dstweek' => 0, + 'dsthour' => 0, + 'dstminute' => 0, + 'dstsecond' => 0, + 'dstmillis' => 0, + 'dstbias' => 0 + ); + + $timezone = $date->toDateTime()->getTimezone(); + list($std, $dst) = self::_getTransitions($timezone, $date); + if ($std) { + $offsets['bias'] = $std['offset'] / 60 * -1; + if ($dst) { + $offsets = self::_generateOffsetsForTransition($offsets, $std, 'std'); + $offsets = self::_generateOffsetsForTransition($offsets, $dst, 'dst'); + $offsets['stdhour'] += $dst['offset'] / 3600; + $offsets['dsthour'] += $std['offset'] / 3600; + $offsets['dstbias'] = ($dst['offset'] - $std['offset']) / 60 * -1; + } + } + + return $offsets; + } + + /** + * Get the transition data for moving from DST to STD time. + * + * @param DateTimeZone $timezone The timezone to get the transition for + * @param Horde_Date $date The date to start from. Really only the + * year we are interested in is needed. + * + * @return array containing the the STD and DST transitions + */ + static protected function _getTransitions($timezone, $date) + { + $std = $dst = null; + // @TODO PHP 5.3 lets you specify a start and end timestamp, probably + // should version sniff here for the improved performance. Just need + // to remember that the first transition structure will then be for + // the start date, so we should go back one year from $date, then ignore + // the first entry. + $transitions = $timezone->getTransitions(); + foreach ($transitions as $i => $transition) { + $d = new Horde_Date($transition['time'], 'UTC'); + if ($d->format('Y') == $date->format('Y')) { + if (isset($transitions[$i + 1])) { + $next = new Horde_Date($transitions[$i + 1]['ts']); + if ($d->format('Y') == $next->format('Y')) { + $dst = $transition['isdst'] ? $transition : $transitions[$i + 1]; + $std = $transition['isdst'] ? $transitions[$i + 1] : $transition; + } else { + $dst = $transition['isdst'] ? $transition: null; + $std = $transition['isdst'] ? null : $transition; + } + } + break; + } elseif ($i == count($transitions) - 1) { + $std = $transition; + } + } + + return array($std, $dst); + } + + /** + * Calculate the offsets for the specified transition + * + * @param array $offsets A TZ offset hash + * @param array $transition A transition hash + * @param string $type Transition type - dst or std + * + * @return array A populated offset hash + */ + static protected function _generateOffsetsForTransition($offsets, $transition, $type) + { + $transitionDate = new Horde_Date($transition['time'], 'UTC'); + $offsets[$type . 'month'] = $transitionDate->format('n'); + $offsets[$type . 'day'] = $transitionDate->format('w'); + $offsets[$type . 'minute'] = (int)$transitionDate->format('i'); + $offsets[$type . 'hour'] = $transitionDate->format('H'); + for ($i = 5; $i > 0; $i--) { + $nth = clone($transitionDate); + $nth->setNthWeekday($transitionDate->format('w'), $i); + if ($transitionDate->compareDate($nth) == 0) { + $offsets[$type . 'week'] = $i; + break; + }; + } + + return $offsets; + } + +} diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml.php b/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml.php new file mode 100644 index 000000000..c4de20d9c --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml.php @@ -0,0 +1,56 @@ + + * @package Horde_ActiveSync + */ + +/** + * File : wbxml.php + * Project : Z-Push + * Descr : WBXML mapping file + * + * Created : 01.10.2007 + * + * � Zarafa Deutschland GmbH, www.zarafaserver.de + * This file is distributed under GPL v2. + * Consult LICENSE file for details + */ +class Horde_ActiveSync_Wbxml +{ + // @TODO - debug should be a config parameter + const DEBUG = false; + const SWITCH_PAGE = 0x00; + const END = 0x01; + const ENTITY = 0x02; + const STR_I = 0x03; + const LITERAL = 0x04; + const EXT_I_0 = 0x40; + const EXT_I_1 = 0x41; + const EXT_I_2 = 0x42; + const PI = 0x43; + const LITERAL_C = 0x44; + const EXT_T_0 = 0x80; + const EXT_T_1 = 0x81; + const EXT_T_2 = 0x82; + const STR_T = 0x83; + const LITERAL_A = 0x84; + const EXT_0 = 0xC0; + const EXT_1 = 0xC1; + const EXT_2 = 0xC2; + const OPAQUE = 0xC3; + const LITERAL_AC = 0xC4; + + const EN_TYPE = 1; + const EN_TAG = 2; + const EN_CONTENT = 3; + const EN_FLAGS = 4; + const EN_ATTRIBUTES = 5; + const EN_TYPE_STARTTAG = 1; + const EN_TYPE_ENDTAG = 2; + const EN_TYPE_CONTENT = 3; + const EN_FLAGS_CONTENT = 1; + const EN_FLAGS_ATTRIBUTES = 2; +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Decoder.php b/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Decoder.php new file mode 100644 index 000000000..d63824421 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Decoder.php @@ -0,0 +1,590 @@ + + * @package Horde_ActiveSync + */ + +/** + * File : wbxml.php + * Project : Z-Push + * Descr : WBXML mapping file + * + * Created : 01.10.2007 + * + * � Zarafa Deutschland GmbH, www.zarafaserver.de + * This file is distributed under GPL v2. + * Consult LICENSE file for details + */ +class Horde_ActiveSync_Wbxml_Decoder +{ + // @TODO + /** + * The DTD + * + * @var array + */ + protected $_dtd; + + /** + * PHP input stream + * + * @var stream + */ + private $_in; + + // These seem to only be used in the Const'r, and I can't find any + // client code that access these properties... + public $version; + public $publicid; + public $publicstringid; + public $charsetid; + + public $stringtable; + + private $_tagcp = 0; + private $_attrcp = 0; + private $_ungetbuffer; + private $_logStack = array(); + + /** + * @var Horde_Log_Logger + */ + private $_logger; + + /** + * Const'r + * + * @param stream $input + * @param array $dtd + * @param array $config + * + * @return Horde_ActiveSync_Wbxml_Decoder + */ + public function __construct($input, $dtd) + { + $this->_in = $input; + $this->_dtd = $dtd; + $this->_logger = new Horde_Support_Stub(); + // @TODO - these don't seem to be used anywhere, do we really need + // to keep them in an instance variable? + $this->version = $this->_getByte(); + $this->publicid = $this->_getMBUInt(); + if ($this->publicid == 0) { + $this->publicstringid = $this->_getMBUInt(); + } + $this->charsetid = $this->_getMBUInt(); + + // This is used, but not sure what the missing getStringTableEntry() + // method was supposed to do yet. + $this->stringtable = $this->_getStringTable(); + } + + public function setLogger(Horde_Log_Logger $logger) + { + $this->_logger = $logger; + } + + /** + * Returns either start, content or end, and auto-concatenates successive content + */ + public function getElement() + { + $element = $this->getToken(); + + switch ($element[Horde_ActiveSync_Wbxml::EN_TYPE]) { + case Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG: + return $element; + case Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG: + return $element; + case Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT: + while (1) { + $next = $this->getToken(); + if ($next == false) { + return false; + } elseif ($next[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_CONTENT) { + $element[Horde_ActiveSync_Wbxml::EN_CONTENT] .= $next[Horde_ActiveSync_Wbxml::EN_CONTENT]; + } else { + $this->_ungetElement($next); + break; + } + } + return $element; + } + + return false; + } + + /** + * + * @return unknown_type + */ + public function peek() + { + $element = $this->getElement(); + $this->_ungetElement($element); + + return $element; + } + + /** + * + * @param $tag + * @return unknown_type + */ + public function getElementStartTag($tag) + { + $element = $this->getToken(); + + if ($element[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG && + $element[Horde_ActiveSync_Wbxml::EN_TAG] == $tag) { + + return $element; + } else { + $this->_logger->debug('Unmatched tag' . $tag . ':'); + $this->_ungetElement($element); + } + + return false; + } + + /** + * + * @return unknown_type + */ + public function getElementEndTag() + { + $element = $this->getToken(); + if ($element[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) { + return $element; + } else { + $this->_logger->err('Unmatched end tag:'); + $this->_logger->err(print_r($element, true)); + //$bt = debug_backtrace(); + // $c = count($bt); + // $this->_logger->err('From ' . $bt[$c-2]['file'] . ':' . $bt[$c - 2]['line']); + $this->_ungetElement($element); + } + + return false; + } + + /** + * + * @return unknown_type + */ + public function getElementContent() + { + $element = $this->getToken(); + if ($element[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT) { + return $element[Horde_ActiveSync_Wbxml::EN_CONTENT]; + } else { + $this->_logger->err('Unmatched content:'); + $this->_logger->err(print_r($element, true)); + $this->_ungetElement($element); + } + + return false; + } + + /** + * @TODO: Do we need? + * @return unknown_type + */ + public function getToken() + { + // See if there's something in the ungetBuffer + if ($this->_ungetbuffer) { + $element = $this->_ungetbuffer; + $this->_ungetbuffer = false; + return $element; + } + + $el = $this->_getToken(); + $this->_logToken($el); + + return $el; + } + + /** + * + * @param unknown_type $el + * @return unknown_type + */ + private function _logToken($el) + { + $spaces = str_repeat(' ', count($this->_logStack)); + switch ($el[Horde_ActiveSync_Wbxml::EN_TYPE]) { + case Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG: + if ($el[Horde_ActiveSync_Wbxml::EN_FLAGS] & Horde_ActiveSync_Wbxml::EN_FLAGS_CONTENT) { + $this->_logger->debug('I ' . $spaces . ' <' . $el[Horde_ActiveSync_Wbxml::EN_TAG] . '>'); + array_push($this->_logStack, $el[Horde_ActiveSync_Wbxml::EN_TAG]); + } else { + $this->_logger->debug('I ' . $spaces . ' <' . $el[Horde_ActiveSync_Wbxml::EN_TAG] . '/>'); + } + break; + case Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG: + $tag = array_pop($this->_logStack); + $this->_logger->debug('I ' . $spaces . ''); + break; + case Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT: + $this->_logger->debug('I ' . $spaces . ' ' . $el[Horde_ActiveSync_Wbxml::EN_CONTENT]); + break; + } + } + + /** + * Returns either a start tag, content or end tag + */ + private function _getToken() { + + // Get the data from the input stream + $element = array(); + + while (1) { + $byte = $this->_getByte(); + + if (!isset($byte)) { + break; + } + + switch ($byte) { + case Horde_ActiveSync_Wbxml::SWITCH_PAGE: + $this->_tagcp = $this->_getByte(); + continue; + + case Horde_ActiveSync_Wbxml::END: + $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG; + return $element; + + case Horde_ActiveSync_Wbxml::ENTITY: + $entity = $this->_getMBUInt(); + $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT; + $element[Horde_ActiveSync_Wbxml::EN_CONTENT] = $this->entityToCharset($entity); + return $element; + + case Horde_ActiveSync_Wbxml::STR_I: + $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT; + $element[Horde_ActiveSync_Wbxml::EN_CONTENT] = $this->_getTermStr(); + return $element; + + case Horde_ActiveSync_Wbxml::LITERAL: + $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG; + $element[Horde_ActiveSync_Wbxml::EN_TAG] = $this->_getStringTableEntry($this->_getMBUInt()); + $element[Horde_ActiveSync_Wbxml::EN_FLAGS] = 0; + return $element; + + case Horde_ActiveSync_Wbxml::EXT_I_0: + case Horde_ActiveSync_Wbxml::EXT_I_1: + case Horde_ActiveSync_Wbxml::EXT_I_2: + $this->_getTermStr(); + // Ignore extensions + continue; + + case Horde_ActiveSync_Wbxml::PI: + // Ignore PI + $this->_getAttributes(); + continue; + + case Horde_ActiveSync_Wbxml::LITERAL_C: + $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG; + $element[Horde_ActiveSync_Wbxml::EN_TAG] = $this->_getStringTableEntry($this->_getMBUInt()); + $element[Horde_ActiveSync_Wbxml::EN_FLAGS] = Horde_ActiveSync_Wbxml::EN_FLAGS_CONTENT; + return $element; + + case Horde_ActiveSync_Wbxml::EXT_T_0: + case Horde_ActiveSync_Wbxml::EXT_T_1: + case Horde_ActiveSync_Wbxml::EXT_T_2: + $this->_getMBUInt(); + // Ingore extensions; + continue; + + case Horde_ActiveSync_Wbxml::STR_T: + $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT; + $element[Horde_ActiveSync_Wbxml::EN_CONTENT] = $this->_getStringTableEntry($this->_getMBUInt()); + return $element; + + case Horde_ActiveSync_Wbxml::LITERAL_A: + $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG; + $element[Horde_ActiveSync_Wbxml::EN_TAG] = $this->_getStringTableEntry($this->_getMBUInt()); + $element[Horde_ActiveSync_Wbxml::EN_ATTRIBUTES] = $this->_getAttributes(); + $element[Horde_ActiveSync_Wbxml::EN_FLAGS] = Horde_ActiveSync_Wbxml::EN_FLAGS_ATTRIBUTES; + return $element; + case Horde_ActiveSync_Wbxml::EXT_0: + case Horde_ActiveSync_Wbxml::EXT_1: + case Horde_ActiveSync_Wbxml::EXT_2: + continue; + + case Horde_ActiveSync_Wbxml::OPAQUE: + $length = $this->_getMBUInt(); + $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT; + $element[Horde_ActiveSync_Wbxml::EN_CONTENT] = $this->_getOpaque($length); + return $element; + + case Horde_ActiveSync_Wbxml::LITERAL_AC: + $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG; + $element[Horde_ActiveSync_Wbxml::EN_TAG] = $this->_getStringTableEntry($this->_getMBUInt()); + $element[Horde_ActiveSync_Wbxml::EN_ATTRIBUTES] = $this->_getAttributes(); + $element[Horde_ActiveSync_Wbxml::EN_FLAGS] = Horde_ActiveSync_Wbxml::EN_FLAGS_ATTRIBUTES | Horde_ActiveSync_Wbxml::EN_FLAGS_CONTENT; + return $element; + + default: + $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG; + $element[Horde_ActiveSync_Wbxml::EN_TAG] = $this->_getMapping($this->_tagcp, $byte & 0x3f); + $element[Horde_ActiveSync_Wbxml::EN_FLAGS] = ($byte & 0x80 ? Horde_ActiveSync_Wbxml::EN_FLAGS_ATTRIBUTES : 0) | ($byte & 0x40 ? Horde_ActiveSync_Wbxml::EN_FLAGS_CONTENT : 0); + if ($byte & 0x80) { + $element[Horde_ActiveSync_Wbxml::EN_ATTRIBUTES] = $this->_getAttributes(); + } + return $element; + } + } + } + + /** + * + * @param $element + * @return unknown_type + */ + public function _ungetElement($element) + { + if ($this->_ungetbuffer) { + $this->_logger->err('Double unget!'); + } + $this->_ungetbuffer = $element; + } + + /** + * + * @return unknown_type + */ + private function _getAttributes() + { + $attributes = array(); + $attr = ''; + + while (1) { + $byte = $this->_getByte(); + if (count($byte) == 0) { + break; + } + + switch($byte) { + case Horde_ActiveSync_Wbxml::SWITCH_PAGE: + $this->_attrcp = $this->_getByte(); + break; + + case Horde_ActiveSync_Wbxml::END: + if ($attr != '') { + $attributes += $this->_splitAttribute($attr); + } + return $attributes; + + case Horde_ActiveSync_Wbxml::ENTITY: + $entity = $this->_getMBUInt(); + $attr .= $this->entityToCharset($entity); + return $element; + + case Horde_ActiveSync_Wbxml::STR_I: + $attr .= $this->_getTermStr(); + return $element; + + case Horde_ActiveSync_Wbxml::LITERAL: + if ($attr != '') { + $attributes += $this->_splitAttribute($attr); + } + $attr = $this->_getStringTableEntry($this->_getMBUInt()); + return $element; + + case Horde_ActiveSync_Wbxml::EXT_I_0: + case Horde_ActiveSync_Wbxml::EXT_I_1: + case Horde_ActiveSync_Wbxml::EXT_I_2: + $this->_getTermStr(); + continue; + + case Horde_ActiveSync_Wbxml::PI: + case Horde_ActiveSync_Wbxml::LITERAL_C: + // Invalid + return false; + + case Horde_ActiveSync_Wbxml::EXT_T_0: + case Horde_ActiveSync_Wbxml::EXT_T_1: + case Horde_ActiveSync_Wbxml::EXT_T_2: + $this->_getMBUInt(); + continue; + + case Horde_ActiveSync_Wbxml::STR_T: + $attr .= $this->_getStringTableEntry($this->_getMBUInt()); + return $element; + + case Horde_ActiveSync_Wbxml::LITERAL_A: + return false; + + case Horde_ActiveSync_Wbxml::EXT_0: + case Horde_ActiveSync_Wbxml::EXT_1: + case Horde_ActiveSync_Wbxml::EXT_2: + continue; + + case Horde_ActiveSync_Wbxml::OPAQUE: + $length = $this->_getMBUInt(); + $attr .= $this->_getOpaque($length); + return $element; + + case Horde_ActiveSync_Wbxml::LITERAL_AC: + return false; + + default: + if ($byte < 128) { + if ($attr != '') { + $attributes += $this->_splitAttribute($attr); + $attr = ''; + } + } + + $attr .= $this->_getMapping($this->_attrcp, $byte); + break; + } + } + + } + + /** + * + * @param $attr + * + * @return unknown_type + */ + private function _splitAttribute($attr) + { + $attributes = array(); + $pos = strpos($attr,chr(61)); // equals sign + if ($pos) { + $attributes[substr($attr, 0, $pos)] = substr($attr, $pos+1); + } else { + $attributes[$attr] = null; + } + + return $attributes; + } + + /** + * + * @return unknown_type + */ + private function _getTermStr() + { + $str = ''; + while(1) { + $in = $this->_getByte(); + + if ($in == 0) { + break; + } else { + $str .= chr($in); + } + } + + return $str; + } + + /** + * + * @param $len + * + * @return unknown_type + */ + private function _getOpaque($len) + { + return fread($this->_in, $len); + } + + /** + * + * @return unknown_type + */ + private function _getByte() + { + $ch = fread($this->_in, 1); + if (strlen($ch) > 0) { + $ch = ord($ch); + $this->_logger->debug('_getByte: ' . $ch); + return $ch; + } else { + return; + } + } + + /** + * + * @return unknown_type + */ + private function _getMBUInt() + { + $uint = 0; + while (1) { + $byte = $this->_getByte(); + $uint |= $byte & 0x7f; + if ($byte & 0x80) { + $uint = $uint << 7; + } else { + break; + } + } + + $this->_logger->debug('_getMBUInt(): ' . $uint); + return $uint; + } + + /** + * + * @return unknown_type + */ + private function _getStringTable() + { + $stringtable = ''; + $length = $this->_getMBUInt(); + if ($length > 0) { + $stringtable = fread($this->_in, $length); + } + + $this->_logger->debug('_getStringTable(): ' . $stringtable); + return $stringtable; + } + + /** + * Really don't know for sure what this method is supposed to do, it is + * called from numerous places in this class, but the original zpush code + * did not contain this method...so, either it's completely broken, or + * normal use-cases do not reach the calling code. Either way, it needs to + * eventually be fixed. + * + * @param unknown_type $id + * + * @return unknown_type + */ + private function _getStringTableEntry($id) + { + throw new Horde_ActiveSync_Exception('Not implemented'); + } + + /** + * + * @param $cp + * @param $id + * @return unknown_type + */ + private function _getMapping($cp, $id) + { + if (!isset($this->_dtd['codes'][$cp]) || !isset($this->_dtd['codes'][$cp][$id])) { + return false; + } else { + if (isset($this->_dtd['namespaces'][$cp])) { + return $this->_dtd['namespaces'][$cp] . ':' . $this->_dtd['codes'][$cp][$id]; + } else { + return $this->_dtd['codes'][$cp][$id]; + } + } + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Encoder.php b/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Encoder.php new file mode 100644 index 000000000..27de7879e --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Encoder.php @@ -0,0 +1,352 @@ + + * @package Horde_ActiveSync + */ + +/** + * File : wbxml.php + * Project : Z-Push + * Descr : WBXML mapping file + * + * Created : 01.10.2007 + * + * � Zarafa Deutschland GmbH, www.zarafaserver.de + * This file is distributed under GPL v2. + * Consult LICENSE file for details + */ +class Horde_ActiveSync_Wbxml_Encoder +{ + private $_dtd; + private $_out; + private $_tagcp; + private $_attrcp; + private $_logStack = array(); + + // We use a delayed output mechanism in which we only output a tag when it actually has something + // in it. This can cause entire XML trees to disappear if they don't have output data in them; Ie + // calling 'startTag' 10 times, and then 'endTag' will cause 0 bytes of output apart from the header. + // Only when content() is called do we output the current stack of tags= + private $_stack = array(); + + /** + * Const'r + * + * @param stream $output + * @param array $dtd + * @param array $config + * + * @return Horde_ActiveSync_Wbxml_Encoder + */ + function __construct($output, $dtd) + { + $this->_out = $output; + $this->_tagcp = 0; + $this->_attrcp = 0; + + // reverse-map the DTD + foreach ($dtd['namespaces'] as $nsid => $nsname) { + $this->_dtd['namespaces'][$nsname] = $nsid; + } + + foreach ($dtd['codes'] as $cp => $value) { + $this->_dtd['codes'][$cp] = array(); + foreach ($dtd['codes'][$cp] as $tagid => $tagname) { + $this->_dtd['codes'][$cp][$tagname] = $tagid; + } + } + } + + public function setLogger(Horde_Log_Logger $logger) + { + $this->_logger = $logger; + } + /** + * + * @return unknown_type + */ + public function startWBXML() + { + header('Content-Type: application/vnd.ms-sync.wbxml'); + $this->_outByte(0x03); // WBXML 1.3 + $this->_outMBUInt(0x01); // Public ID 1 + $this->_outMBUInt(106); // UTF-8 + $this->_outMBUInt(0x00); // string table length (0) + } + + /** + * + * @param $tag + * @param $attributes + * @param $nocontent + * @return void + */ + public function startTag($tag, $attributes = false, $nocontent = false) + { + $stackelem = array(); + if (!$nocontent) { + $stackelem['tag'] = $tag; + $stackelem['attributes'] = $attributes; + $stackelem['nocontent'] = $nocontent; + $stackelem['sent'] = false; + + array_push($this->_stack, $stackelem); + + // If 'nocontent' is specified, then apparently the user wants to force + // output of an empty tag, and we therefore output the stack here + } else { + $this->_outputStack(); + $this->_startTag($tag, $attributes, $nocontent); + } + } + + /** + * + * @return void + */ + public function endTag() + { + $stackelem = array_pop($this->_stack); + // Only output end tags for items that have had a start tag sent + if ($stackelem['sent']) { + $this->_endTag(); + } + } + + /** + * + * @param $content + * + * @return void + */ + public function content($content) + { + // We need to filter out any \0 chars because it's the string terminator in WBXML. We currently + // cannot send \0 characters within the XML content anywhere. + $content = str_replace('\0', '', $content); + if ('x' . $content == 'x') { + return; + } + $this->_outputStack(); + $this->_content($content); + } + + /** + * Output any tags on the stack that haven't been output yet + * + * @TODO: Not 100% sure this can be private + */ + private function _outputStack() + { + for ($i=0; $i < count($this->_stack); $i++) { + if (!$this->_stack[$i]['sent']) { + $this->_startTag($this->_stack[$i]['tag'], $this->_stack[$i]['attributes'], $this->_stack[$i]['nocontent']); + $this->_stack[$i]['sent'] = true; + } + } + } + + // Outputs an actual start tag + private function _startTag($tag, $attributes = false, $nocontent = false) + { + $this->_logStartTag($tag, $attributes, $nocontent); + $mapping = $this->_getMapping($tag); + if (!$mapping) { + return false; + } + + if ($this->_tagcp != $mapping['cp']) { + $this->_outSwitchPage($mapping['cp']); + $this->_tagcp = $mapping['cp']; + } + + $code = $mapping['code']; + if (isset($attributes) && is_array($attributes) && count($attributes) > 0) { + $code |= 0x80; + } + + if (!isset($nocontent) || !$nocontent) { + $code |= 0x40; + } + + $this->_outByte($code); + if ($code & 0x80) { + $this->_outAttributes($attributes); + } + } + + /** + * Outputs data + * + * @param $content + * + * @return unknown_type + */ + private function _content($content) + { + $this->_logContent($content); + $this->_outByte(Horde_ActiveSync_Wbxml::STR_I); + $this->_outTermStr($content); + } + + // Outputs an actual end tag + function _endTag() { + $this->_logEndTag(); + $this->_outByte(Horde_ActiveSync_Wbxml::END); + } + + /** + * + * @param $byte + * @return unknown_type + */ + private function _outByte($byte) + { + fwrite($this->_out, chr($byte)); + } + + /** + * + * @param $uint + * @return unknown_type + */ + private function _outMBUInt($uint) + { + while (1) { + $byte = $uint & 0x7f; + $uint = $uint >> 7; + if ($uint == 0) { + $this->_outByte($byte); + break; + } else { + $this->_outByte($byte | 0x80); + } + } + } + + /** + * + * @param $content + * @return unknown_type + */ + private function _outTermStr($content) + { + fwrite($this->_out, $content); + fwrite($this->_out, chr(0)); + } + + /** + * + * @return unknown_type + */ + private function _outAttributes() + { + // We don't actually support this, because to do so, we would have + // to build a string table before sending the data (but we can't + // because we're streaming), so we'll just send an END, which just + // terminates the attribute list with 0 attributes. + $this->_outByte(Horde_ActiveSync_Wbxml::END); + } + + /** + * + * @param $page + * @return unknown_type + */ + private function _outSwitchPage($page) + { + $this->_outByte(Horde_ActiveSync_Wbxml::SWITCH_PAGE); + $this->_outByte($page); + } + + /** + * + * @param $tag + * @return unknown_type + */ + private function _getMapping($tag) + { + $mapping = array(); + $split = $this->_splitTag($tag); + if (isset($split['ns'])) { + $cp = $this->_dtd['namespaces'][$split['ns']]; + } else { + $cp = 0; + } + + $code = $this->_dtd['codes'][$cp][$split['tag']]; + $mapping['cp'] = $cp; + $mapping['code'] = $code; + + return $mapping; + } + + /** + * + * @param $fulltag + * @return unknown_type + */ + private function _splitTag($fulltag) + { + $ns = false; + $pos = strpos($fulltag, chr(58)); // chr(58) == ':' + if ($pos) { + $ns = substr($fulltag, 0, $pos); + $tag = substr($fulltag, $pos+1); + } else { + $tag = $fulltag; + } + + $ret = array(); + if ($ns) { + $ret['ns'] = $ns; + } + $ret['tag'] = $tag; + + return $ret; + } + + /** + * + * @param $tag + * @param $attr + * @param $nocontent + * @return unknown_type + */ + private function _logStartTag($tag, $attr, $nocontent) + { + $spaces = str_repeat(' ', count($this->_logStack)); + if ($nocontent) { + $this->_logger->debug(sprintf('O %s <%s/>', $spaces, $tag)); + } else { + array_push($this->_logStack, $tag); + $this->_logger->debug(sprintf('O %s <%s>', $spaces, $tag)); + } + } + + /** + * + * @return unknown_type + */ + private function _logEndTag() + { + $spaces = str_repeat(' ', count($this->_logStack)); + $tag = array_pop($this->_logStack); + $this->_logger->debug(sprintf('O %s <%s/>', $spaces, $tag)); + } + + /** + * + * @param unknown_type $content + * @return unknown_type + */ + private function _logContent($content) + { + $spaces = str_repeat(' ', count($this->_logStack)); + $this->_logger->debug('O ' . $spaces . $content); + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/package.xml b/framework/ActiveSync/package.xml new file mode 100644 index 000000000..dc090438b --- /dev/null +++ b/framework/ActiveSync/package.xml @@ -0,0 +1,131 @@ + + + ActiveSync + pear.horde.org + Horde ActiveSync Server Library + This package provides libraries for implementing an ActiveSync server. + + Michael J. Rubinsky + mrubinsk + mrubinsk@horde.org + yes + + 2010-01-10 + + 0.1.0 + 0.1.0 + + + alpha + alpha + + GPLv2 + +* Initial release + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5.2.0 + + + 1.5.0 + + + Date + pear.horde.org + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/ActiveSync/test/Horde/ActiveSync/AllTests.php b/framework/ActiveSync/test/Horde/ActiveSync/AllTests.php new file mode 100644 index 000000000..9e7ff4819 --- /dev/null +++ b/framework/ActiveSync/test/Horde/ActiveSync/AllTests.php @@ -0,0 +1,35 @@ + + */ + +/** + * Define the main method + */ +if (!defined('PHPUnit_MAIN_METHOD')) { + define('PHPUnit_MAIN_METHOD', 'Horde_ActiveSync_AllTests::main'); +} + +/** + * Prepare the test setup. + */ +require_once 'Horde/Test/AllTests.php'; + +/** + * @package Horde_ActiveSync + * @subpackage UnitTests + */ +class Horde_ActiveSync_AllTests extends Horde_Test_AllTests +{ + +} + +if (PHPUnit_MAIN_METHOD == 'Horde_ActiveSync_AllTests::main') { + Horde_ActiveSync_AllTests::main('Horde_ActiveSync', __FILE__); +} diff --git a/framework/ActiveSync/test/Horde/ActiveSync/FileStateTest.php b/framework/ActiveSync/test/Horde/ActiveSync/FileStateTest.php new file mode 100644 index 000000000..ebc6c2c77 --- /dev/null +++ b/framework/ActiveSync/test/Horde/ActiveSync/FileStateTest.php @@ -0,0 +1,136 @@ + + * @category Horde + * @package Horde_ActiveSync + */ +require_once dirname(__FILE__) . '/fixtures/MockConnector.php'; + +//FIXME: This can be removed once all the constants are class-constants +require_once dirname(__FILE__) . '/../../../lib/Horde/ActiveSync.php'; +class Horde_ActiveSync_FileStateTest extends Horde_Test_Case +{ + /** + * Tests initial state loading from synckey zero. + * Should initialize the initial server state. + */ + public function testCollectionSyncState() + { + $state = new Horde_ActiveSync_State_File(array('stateDir' => './')); + $state->init(array('id' => 'Contacts', + 'class' => 'Contacts')); + + $state->loadState(0); + + $contact = array( + '__key' => '9b07c14b086932e69cc7eb1baed0cc87', + '__owner' => 'mike', + '__type' => 'Object', + '__members' => '', + '__uid' => '20070112030611.62g1lg5nry80@test.theupstairsroom.com', + 'firstname' => 'Michael', + 'lastname' => 'Rubinsky', + 'middlenames' => 'Joseph', + 'namePrefix' => 'Dr', + 'nameSuffix' => 'PharmD', + 'name' => 'Michael Joseph Rubinsky', + 'alias' => 'Me', + 'birthday' => '1970-03-20', + 'homeStreet' => '123 Main St.', + 'homePOBox' => '', + 'homeCity' => 'Anywhere', + 'homeProvince' => 'NJ', + 'homePostalCode' => '08080', + 'homeCountry' => 'US', + 'workStreet' => 'Kings Hwy', + 'workPOBox' => '', + 'workCity' => 'Somewhere', + 'workProvince' => 'NJ', + 'workPostalCode' => '08052', + 'workCountry' => 'US', + 'timezone' => 'America/New_York', + 'email' => 'mrubinsk@horde.org', + 'homePhone' => '(856)555-1234', + 'workPhone' => '(856)555-5678', + 'cellPhone' => '(609)555-9876', + 'fax' => '', + 'pager' => '', + 'title' => '', + 'role' => '', + 'company' => '', + 'category' => '', + 'notes' => '', + 'website' => '', + 'freebusyUrl' => '', + 'pgpPublicKey' => '', + 'smimePublicKey' => '', + ); + + /* Create a mock driver with desired return values */ + $fixture = array('contacts_list' => array('20070112030611.62g1lg5nry80@test.theupstairsroom.com'), + 'contacts_getActionTimestamp' => 0, + 'contacts_export' => $contact); + + $connector = new Horde_ActiveSync_MockConnector(array('fixture' => $fixture)); + $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector, + 'state_basic' => $state)); + $state->setBackend($driver); + $state->setLogger(new Horde_Support_Stub()); + + /* Get the current state from the "server" */ + $changes = $state->getChanges(); + $this->assertEquals(1, $state->getChangeCount()); + $this->assertEquals(array('type' => 'change', 'flags' => 'NewMessage', 'id' => '20070112030611.62g1lg5nry80@test.theupstairsroom.com'), $changes[0]); + + /* Import the state into the state object */ + foreach($changes as $change) { + // We know it's always a 'change' since the above test passed + $stat = $driver->StatMessage('Contacts', $change['id'], 0); + $state->updateState('change', $stat, 0); + } + + /* Get and set the new synckey */ + $key = $state->getNewSyncKey(0); + $state->setNewSyncKey($key); + $state->save(); + + /* Check that the state was saved to file */ + $this->assertFileExists($key); + + /* ...and check that it contains the serialized state data + * by reading it into a new object and performing another diff */ + $newstate = new Horde_ActiveSync_State_File(array('stateDir' => './')); + $newstate->init(array('id' => 'Contacts', + 'class' => 'Contacts')); + $newstate->setBackend($driver); + $newstate->setLogger(new Horde_Support_Stub()); + $newstate->loadState($key); + $this->assertEquals(0, $newstate->getChangeCount()); + + /* Clean up */ + unlink($key); + } + + public function testFolderSyncState() + { + // TODO + } + + public function testConflicts() + { + // TODO + } + + public function testPingState() + { + // TODO + } + + public function testPingStateUpdatedAfterStateUpdate() + { + + } +} + diff --git a/framework/ActiveSync/test/Horde/ActiveSync/HordeDriverTest.php b/framework/ActiveSync/test/Horde/ActiveSync/HordeDriverTest.php new file mode 100644 index 000000000..46011df7a --- /dev/null +++ b/framework/ActiveSync/test/Horde/ActiveSync/HordeDriverTest.php @@ -0,0 +1,270 @@ + + * @category Horde + * @package Horde_ActiveSync + */ +require_once dirname(__FILE__) . '/fixtures/MockConnector.php'; +class Horde_ActiveSync_HordeDriverTest extends Horde_Test_Case +{ + /** + * + */ + public function testConnectorRequiresRegistry() + { + $this->setExpectedException('Horde_ActiveSync_Exception'); + $connector = new Horde_ActiveSync_Driver_Horde_Connector_Registry(); + + $registry = $this->getMockSkipConstructor('Horde_Registry'); + $connector = new Horde_ActiveSync_Driver_Horde_Connector_Registry(array('registry' => $registry)); + $this->assertType('Horde_ActiveSync_Driver_Horde_Connector_Registry', $connectory); + } + + public function testDriverRequiresConnector() + { + $this->setExpectedException('Horde_ActiveSync_Exception'); + $driver = new Horde_ActiveSync_Driver_Horde(); + + // Now for real + $registry = $this->getMockSkipConstructor('Horde_Registry'); + $connector = new Horde_ActiveSync_Driver_Horde_Connector_Registry(array('registry' => $registry)); + $state = $this->getMockSkipConstructor('Horde_ActiveSync_State_File'); + $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector, + 'state_basic' => $state)); + $this->assertType('Horde_ActiveSync_Driver', $driver); + } + + /** + * Test that Horde_ActiveSync_Driver_Horde#getMessageList() returns expected + * data structures. Uses mock data via the MockConnector + * + */ + public function testGetMessageList() + { + // Test Contacts - simulates returning two contacts, both of which have no history modify entries. + $fixture = array('contacts_list' => array('20070112030603.249j42k3k068@test.theupstairsroom.com', + '20070112030611.62g1lg5nry80@test.theupstairsroom.com'), + 'contacts_getActionTimestamp' => 0); + + $connector = new Horde_ActiveSync_MockConnector(array('fixture' => $fixture)); + $state = $this->getMockSkipConstructor('Horde_ActiveSync_State_File'); + $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector, + 'state_basic' => $state)); + + $results = $driver->getMessageList('Contacts', time()); + + //$expected = array( + // array('id' => '20070112030603.249j42k3k068@test.theupstairsroom.com', + // 'mod' => 0, + // 'flags' => 1), + // array('id' => '20070112030611.62g1lg5nry80@test.theupstairsroom.com', + // 'mod' => 0, + // 'flags' => 1) + //); + + $this->assertEquals(2, count($results)); + foreach ($results as $result) { + if ($result['id'] != '20070112030603.249j42k3k068@test.theupstairsroom.com') { + $this->assertEquals('20070112030611.62g1lg5nry80@test.theupstairsroom.com', $result['id']); + } else { + $this->assertEquals('20070112030603.249j42k3k068@test.theupstairsroom.com', $result['id']); + } + } + } + + /** + * Test retrieving a message from storage. + * Mocks the registry response. + * + * @TODO: Calendar data, more complete test of vCard attribtues. + * + */ + public function testGetMessage() + { + require_once 'Horde/ActiveSync.php'; + + $contact = array( + '__key' => '9b07c14b086932e69cc7eb1baed0cc87', + '__owner' => 'mike', + '__type' => 'Object', + '__members' => '', + '__uid' => '20100205111228.89913meqtp5u09rg@localhost', + 'firstname' => 'Michael', + 'lastname' => 'Rubinsky', + 'middlenames' => 'Joseph', + 'namePrefix' => 'Dr', + 'nameSuffix' => 'PharmD', + 'name' => 'Michael Joseph Rubinsky', + 'alias' => 'Me', + 'birthday' => '1970-03-20', + 'homeStreet' => '123 Main St.', + 'homePOBox' => '', + 'homeCity' => 'Anywhere', + 'homeProvince' => 'NJ', + 'homePostalCode' => '08080', + 'homeCountry' => 'US', + 'workStreet' => 'Kings Hwy', + 'workPOBox' => '', + 'workCity' => 'Somewhere', + 'workProvince' => 'NJ', + 'workPostalCode' => '08052', + 'workCountry' => 'US', + 'timezone' => 'America/New_York', + 'email' => 'mrubinsk@horde.org', + 'homePhone' => '(856)555-1234', + 'workPhone' => '(856)555-5678', + 'cellPhone' => '(609)555-9876', + 'fax' => '', + 'pager' => '', + 'title' => '', + 'role' => '', + 'company' => '', + 'category' => '', + 'notes' => '', + 'website' => '', + 'freebusyUrl' => '', + 'pgpPublicKey' => '', + 'smimePublicKey' => '', + ); + + // Need to init the Nls system + error_reporting(E_ALL & ~E_DEPRECATED); + require_once dirname(__FILE__) . '/../../../../../horde/lib/core.php'; + Horde_Nls::setLanguage(); + + $fixture = array('contacts_export' => $contact); + $connector = new Horde_ActiveSync_MockConnector(array('fixture' => $fixture)); + $state = $this->getMockSkipConstructor('Horde_ActiveSync_State_File'); + $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector, + 'state_basic' => $state)); + + $results = $driver->getMessage(Horde_ActiveSync_Driver_Horde::CONTACTS_FOLDER, + '20070112030603.249j42k3k068@test.theupstairsroom.com', + 0); + $this->assertType('Horde_ActiveSync_Message_Contact', $results); + $this->assertEquals('Dr', $results->title); + $this->assertEquals('PharmD', $results->suffix); + $this->assertEquals('Michael Joseph Rubinsky', $results->fileas); + $this->assertEquals('mrubinsk@horde.org', $results->email1address); + $this->assertEquals('6757200', $results->birthday); + $this->assertEquals('(856)555-1234', $results->homephonenumber); + $this->assertEquals('(856)555-5678', $results->businessphonenumber); + $this->assertEquals('(609)555-9876', $results->mobilephonenumber); + $this->assertEquals('123 Main St.', $results->homestreet); + $this->assertEquals('Anywhere', $results->homecity); + $this->assertEquals('NJ', $results->homestate); + $this->assertEquals('08080', $results->homepostalcode); + $this->assertEquals('US', $results->homecountry); + $this->assertEquals('Kings Hwy', $results->businessstreet); + $this->assertEquals('Somewhere', $results->businesscity); + $this->assertEquals('NJ', $results->businessstate); + $this->assertEquals('08052', $results->businesspostalcode); + $this->assertEquals('US', $results->businesscountry); + } + + /** + * Test that the values present in the contact message are always utf-8. + */ + public function testStreamerUTF8() + { + // Need to init the Nls system + error_reporting(E_ALL & ~E_DEPRECATED); + require_once dirname(__FILE__) . '/../../../../../horde/lib/core.php'; + Horde_Nls::setLanguage(); + + $contact = array( + '__uid' => '20100205111228.89913meqtp5u09rg@localhost', + 'firstname' => Horde_String::convertCharset('Grüb', Horde_Nls::getCharset(), 'iso-8859-1') + ); + + $fixture = array('contacts_export' => $contact); + $connector = new Horde_ActiveSync_MockConnector(array('fixture' => $fixture)); + $state = $this->getMockSkipConstructor('Horde_ActiveSync_State_File'); + $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector, + 'state_basic' => $state)); + + $results = $driver->getMessage(Horde_ActiveSync_Driver_Horde::CONTACTS_FOLDER, + '20070112030603.249j42k3k068@test.theupstairsroom.com', + 0); + + $this->assertEquals(Horde_String::convertCharset('Grüb', Horde_Nls::getCharset(), 'utf-8'), $results->firstname); + } + /** + * Test ChangeMessage: + * This tests converting the contact streamer object to a hash suitable for + * passing to the contacts/import method. Because it only returns the UID + * for the newly added/edited entry, we can't check the results here. The + * check is done in the MockConnector object, throwing an exception if it + * fails. + * + */ + public function testChangeMessage() + { + // fixtures + $message = new Horde_ActiveSync_Message_Contact(); + $message->fileas = 'Michael Joseph Rubinsky'; + $message->firstname = 'Michael'; + $message->lastname = 'Rubinsky'; + $message->middlename = 'Joseph'; + $message->birthday = '6757200'; + $message->email1address = 'mrubinsk@horde.org'; + $message->homephonenumber = '(856)555-1234'; + $message->businessphonenumber = '(856)555-5678'; + $message->mobilephonenumber = '(609)555-9876'; + $message->homestreet = '123 Main St.'; + $message->homecity = 'Anywhere'; + $message->homestate = 'NJ'; + $message->homepostalcode = '08080'; + + // Need to init the Nls system + error_reporting(E_ALL & ~E_DEPRECATED); + require_once dirname(__FILE__) . '/../../../../../horde/lib/core.php'; + Horde_Nls::setLanguage(); + + $connector = new Horde_ActiveSync_MockConnector(array('fixture' => array())); + $state = $this->getMockSkipConstructor('Horde_ActiveSync_State_File'); + $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector, + 'state_basic' => $state)); + + try { + $results = $driver->ChangeMessage(Horde_ActiveSync_Driver_Horde::CONTACTS_FOLDER, + 0, $message); + } catch (Horde_ActiveSync_Exception $e) { + $this->fail($e->getMessage()); + } + } + + /** + * Test return structure of GetFolderList command + */ + public function testGetFolderList() + { + $registry = $this->getMockSkipConstructor('Horde_Registry'); + $state = $this->getMockSkipConstructor('Horde_ActiveSync_State_File'); + $connector = new Horde_ActiveSync_MockConnector(array('fixture' => array())); + $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector, + 'state_basic' => $state)); + $results = $driver->getFolderList(); + $expected = array( + array( + 'id' => 'Calendar', + 'mod' => 'Calendar', + 'parent' => 0, + ), + array( + 'id' => 'Contacts', + 'mod' => 'Contacts', + 'parent' => 0 + ), + array( + 'id' => 'Tasks', + 'mod' => 'Tasks', + 'parent' => 0 + ) + ); + + $this->assertEquals($expected, $results); + } +} diff --git a/framework/ActiveSync/test/Horde/ActiveSync/TimezoneTest.php b/framework/ActiveSync/test/Horde/ActiveSync/TimezoneTest.php new file mode 100644 index 000000000..319b3f7dc --- /dev/null +++ b/framework/ActiveSync/test/Horde/ActiveSync/TimezoneTest.php @@ -0,0 +1,123 @@ + + * @category Horde + * @package Horde_ActiveSync + */ +class Horde_ActiveSync_TimezoneTest extends Horde_Test_Case +{ + /** + * Test building an Offset hash from a given ActiveSync style base64 encoded + * timezone structure. + */ + public function testOffsetsFromSyncTZ() + { + // America/Los_Angeles GMT-8:00 + $blob = '4AEAACgARwBNAFQALQAwADgAOgAwADAAKQAgAFAAYQBjAGkAZgBpAGMAIABUAGkAbQBlACAAKABVAFMAIAAmACAAQwAAA AsAAAABAAIAAAAAAAAAAAAAACgARwBNAFQALQAwADgAOgAwADAAKQAgAFAAYQBjAGkAZgBpAGMAIABUAGkAbQBlACAAKA BVAFMAIAAmACAAQwAAAAMAAAACAAIAAAAAAAAAxP///w=='; + $tz = Horde_ActiveSync_Timezone::getOffsetsFromSyncTZ($blob); + + $expected = array( + 'bias' => 480, + //'stdname' => '(GMT-08:00) Pacific Time (US & C', + 'stdyear' => 0, + 'stdmonth' => 11, + 'stdday' => 0, + 'stdweek' => 1, + 'stdhour' => 2, + 'stdminute' => 0, + 'stdsecond' => 0, + 'stdmillis' => 0, + 'stdbias' => 0, + 'dstyear' => 0, + 'dstmonth' => 3, + 'dstday' => 0, + 'dstweek' => 2, + 'dsthour' => 2, + 'dstminute' => 0, + 'dstsecond' => 0, + 'dstmillis' => 0, + 'dstbias' => -60, + 'timezone' => 480, + 'timezonedst' => -60 + ); + + foreach ($expected as $key => $value) { + $this->assertEquals($value, $tz[$key]); + } + } + + /** + * Test creating a Offset hash for a given timezone. + */ + public function testGetOffsetsFromDate() + { + // The actual time doesn't matter, we really only need a year and a + // timezone that we are interested in. + $date = new Horde_Date(time(), 'America/Los_Angeles'); + $tz = Horde_ActiveSync_Timezone::getOffsetsFromDate($date); + + /* We don't set the name here */ + $expected = array( + 'bias' => 480, + 'stdname' => '', + 'stdyear' => 0, + 'stdmonth' => 11, + 'stdday' => 0, + 'stdweek' => 1, + 'stdhour' => 2, + 'stdminute' => 0, + 'stdsecond' => 0, + 'stdmillis' => 0, + 'stdbias' => 0, + 'dstname' => '', + 'dstyear' => 0, + 'dstmonth' => 3, + 'dstday' => 0, + 'dstweek' => 2, + 'dsthour' => 2, + 'dstminute' => 0, + 'dstsecond' => 0, + 'dstmillis' => 0, + 'dstbias' => -60, + ); + + foreach ($expected as $key => $value) { + $this->assertEquals($value, $tz[$key]); + } + } + + /** + * Test generating an ActiveSync TZ structure given a TZ Offset hash + */ + public function testGetSyncTZFromOffsets() + { + $offsets = array( + 'bias' => 480, + 'stdname' => '', + 'stdyear' => 0, + 'stdmonth' => 11, + 'stdday' => 0, + 'stdweek' => 1, + 'stdhour' => 2, + 'stdminute' => 0, + 'stdsecond' => 0, + 'stdmillis' => 0, + 'stdbias' => 0, + 'dstname' => '', + 'dstyear' => 0, + 'dstmonth' => 3, + 'dstday' => 0, + 'dstweek' => 2, + 'dsthour' => 2, + 'dstminute' => 0, + 'dstsecond' => 0, + 'dstmillis' => 0, + 'dstbias' => -60, + ); + + $tz = Horde_ActiveSync_Timezone::getSyncTZFromOffsets($offsets); + $this->assertEquals('4AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==', $tz); + } +} \ No newline at end of file diff --git a/framework/ActiveSync/test/Horde/ActiveSync/fixtures/MockConnector.php b/framework/ActiveSync/test/Horde/ActiveSync/fixtures/MockConnector.php new file mode 100644 index 000000000..5e9456b9c --- /dev/null +++ b/framework/ActiveSync/test/Horde/ActiveSync/fixtures/MockConnector.php @@ -0,0 +1,76 @@ + + * @category Horde + * @package Horde_ActiveSync + */ +class Horde_ActiveSync_MockConnector +{ + public function __construct($params = array()) + { + $this->_fixture = $params['fixture']; + } + + public function __call($name, $args) + { + if (empty($this->_fixture[$name])) { + return 0; + } + return $this->_fixture[$name]; + } + + public function contacts_import($content, $content_type) + { + // $content is a vCard that should eq: + $expected = array( + 'firstname' => 'Michael', + 'lastname' => 'Rubinsky', + 'middlenames' => 'Joseph', + 'namePrefix' => '', + 'nameSuffix' => '', + 'name' => 'Michael Joseph Rubinsky', + 'birthday' => '1970-03-20', + 'homeStreet' => '123 Main St.', + 'homeCity' => 'Anywhere', + 'homeProvince' => 'NJ', + 'homePostalCode' => '08080', + 'homeCountry' => '', + 'workStreet' => '', + 'workCity' => '', + 'workProvince' => '', + 'workCountry' => '', + //'timezone' => '', + 'email' => 'mrubinsk@horde.org', + 'homePhone' => '(856)555-1234', + 'workPhone' => '(856)555-5678', + 'cellPhone' => '(609)555-9876', + 'fax' => '', + 'pager' => '', + 'title' => '', + 'company' => '', + //'category' => '', + 'notes' => '', + 'website' => '', + ); + + foreach ($expected as $key => $value) { + if ($content[$key] != $value) { + throw new Horde_ActiveSync_Exception('Expected value ' . $value . ' did not match received value ' . $content[$key]); + } + } + + return 'xx.xx@localhost'; + } + + public function contacts_replace() + { + + } + + public function calendar_import() + { + + } +} \ No newline at end of file diff --git a/framework/Date/lib/Horde/Date.php b/framework/Date/lib/Horde/Date.php index a72596dd7..e5103d7a1 100644 --- a/framework/Date/lib/Horde/Date.php +++ b/framework/Date/lib/Horde/Date.php @@ -476,6 +476,7 @@ class Horde_Date } else { $d->sec += $factor; } + return $d; } diff --git a/framework/Rpc/lib/Horde/Rpc.php b/framework/Rpc/lib/Horde/Rpc.php index e33bdfb5d..8fdc5dbae 100644 --- a/framework/Rpc/lib/Horde/Rpc.php +++ b/framework/Rpc/lib/Horde/Rpc.php @@ -50,23 +50,51 @@ class Horde_Rpc var $_requestMissingAuthorization = true; /** + * Request variables, cookies etc... + * + * @var Horde_Controller_Request_Http + */ + protected $_request; + + /** + * Logging + * + * @var Horde_Log_Logger + */ + protected $_logger; + + /** * RPC server constructor. * + * @param Horde_Controller_Request_Http The request object + * * @param array $config A hash containing any additional configuration or * connection parameters a subclass might need. * * @return Horde_Rpc An RPC server instance. */ - public function __construct($params = array()) + public function __construct($request, $params = array()) { - $this->_params = $params; + // Create a stub if we don't have a useable logger. + if (isset($params['logger']) + && is_callable(array($params['logger'], 'log'))) { + $this->_logger = $params['logger']; + unset($params['logger']); + } else { + $this->_logger = new Horde_Support_Stub; + } + $this->_params = $params; + $this->_request = $request; + if (isset($params['requireAuthorization'])) { $this->_requireAuthorization = $params['requireAuthorization']; } if (isset($params['requestMissingAuthorization'])) { $this->_requestMissingAuthorization = $params['requestMissingAuthorization']; } + + $this->_logger->debug('Horde_Rpc::__construct complete'); } /** @@ -81,17 +109,19 @@ class Horde_Rpc */ function authorize() { + $this->_logger->debug('Horde_Rpc::authorize() starting'); if (!$this->_requireAuthorization) { return true; } + // @TODO: inject this $auth = Horde_Auth::singleton($GLOBALS['conf']['auth']['driver']); - if (isset($_SERVER['PHP_AUTH_USER'])) { - $user = $_SERVER['PHP_AUTH_USER']; - $pass = $_SERVER['PHP_AUTH_PW']; - } elseif (isset($_SERVER['Authorization'])) { - $hash = str_replace('Basic ', '', $_SERVER['Authorization']); + if ($this->_request->getServer('PHP_AUTH_USER')) { + $user = $this->_request->getServer('PHP_AUTH_USER'); + $pass = $this->_request->getServer('PHP_AUTH_PW'); + } elseif ($this->_request->getServer('Authorization')) { + $hash = str_replace('Basic ', '', $this->_request->getServer('Authorization')); $hash = base64_decode($hash); if (strpos($hash, ':') !== false) { list($user, $pass) = explode(':', $hash, 2); @@ -108,6 +138,7 @@ class Horde_Rpc exit; } + $this->_logger->debug('Horde_Rpc::authorize() exiting'); return true; } @@ -154,6 +185,22 @@ class Horde_Rpc } /** + * Send the output back to the client + * + * @param string $output The output to send back to the client. Can be + * overridden in classes if needed. + * + * @return void + */ + function sendOutput($output) + { + header('Content-Type: ' . $this->getResponseContentType()); + header('Content-length: ' . strlen($output)); + header('Accept-Charset: UTF-8'); + echo $output; + } + + /** * Builds an RPC request and sends it to the RPC server. * * This statically called method is actually the RPC client. @@ -192,12 +239,12 @@ class Horde_Rpc * @return Horde_Rpc The newly created concrete Horde_Rpc server instance, * or an exception if there is an error. */ - public static function factory($driver, $params = null) + public static function factory($driver, $request, $params = null) { $driver = basename($driver); $class = 'Horde_Rpc_' . $driver; if (class_exists($class)) { - return new $class($params); + return new $class($request, $params); } else { throw new Horde_Rpc_Exception('Class definition of ' . $class . ' not found.'); } diff --git a/framework/Rpc/lib/Horde/Rpc/ActiveSync.php b/framework/Rpc/lib/Horde/Rpc/ActiveSync.php new file mode 100644 index 000000000..7e8d5ea5a --- /dev/null +++ b/framework/Rpc/lib/Horde/Rpc/ActiveSync.php @@ -0,0 +1,264 @@ + + * + * @category Horde + * @package Horde_Rpc + */ +class Horde_Rpc_ActiveSync extends Horde_Rpc +{ + /** + * Holds the request's GET variables + * + * @var array + */ + private $_get; + + /** + * The ActiveSync server object + * + * @var Horde_ActiveSync + */ + private $_server; + + /** + * ActiveSync's backend target (the datastore it syncs the PDA with) + * + * @var Horde_ActiveSync_Driver + */ + private $_backend; + + /** + * Policy key + * (Sent by client either as 'X-Ms-Policykey' or 'X-MS-PolicyKey') + * + * @var string + */ + private $_policykey; + + /** + * ActiveSync protocol version + * (Sent as either 'Ms-Asprotocolversion' or 'MS-ASProtocolVersion') + * Should default to '1.0' if not sent. + * + * @var string + */ + private $_protocolVersion; + + /** + * Require provisioning? Valid values: + * true - provisioning required + * false - not checked + * loose - Allows the 'loose enforcement' of the provisioning policies for + * older devices which don't support provisioning + * + * @var mixed + */ + private $_provisioning; + + /** + * Constructor. + * Parameters in addition to Horde_Rpc's: + * (required) 'backend' = Horde_ActiveSync_Driver + * (required) 'server' = Horde_ActiveSync + * (optional) 'provisioning' = Require device provisioning? + * + * @param Horde_Controller_Request_Http The request object. + * @param array $config A hash containing any additional configuration or + * connection parameters this class might need. + */ + public function __construct($request, $params = array()) + { + parent::__construct($request, $params); + + /* Check for requirements */ + $this->_get = $request->getGetParams(); + if ($request->getMethod() == 'POST' && + (empty($this->_get['Cmd']) || empty($this->_get['User']) || empty($this->_get['DeviceId']) || empty($this->_get['DeviceType']))) { + + $this->_logger->err('Missing required parameters.'); + + throw new Horde_Rpc_Exception('Your device requested the ActiveSync URL wihtout required parameters.'); + } + + /* Set our server and backend objects */ + $this->_backend = $params['backend']; + $this->_server = $params['server']; + + /* provisioning can be false, true, or 'loose' */ + $this->_provisioning = empty($params['provisioning']) ? false : $params['provisioning']; + if ($this->_provisioning) { + $this->_policykey = $this->_server->getPolicyKey(); + } + $this->_server->setProvisioning = $this->_provisioning; + + /* Protocol Version */ + $this->_protocolVersion = $this->_server->getProtocolVersion(); + } + + /** + * Returns the Content-Type of the response. + * + * @return string The MIME Content-Type of the RPC response. + */ + public function getResponseContentType() + { + return 'application/vnd.ms-sync.wbxml'; + } + + /** + * Horde_ActiveSync will read the input stream directly, do not access + * it here. + * + * @see framework/Rpc/lib/Horde/Horde_Rpc#getInput() + */ + public function getInput() + { + return null; + } + + /** + * Sends an RPC request to the server and returns the result. + * + * @param string $request PHP input stream (ignored). + * + * @return void + */ + public function getResponse($request) + { + /* Not sure about this, but it's what zpush did so... */ + ob_start(null, 1048576); + + switch ($this->_request->getServer('REQUEST_METHOD')) { + case 'OPTIONS': + $this->_logger->debug('Horde_Rpc_ActiveSync::getResponse() starting for OPTIONS'); + $this->_server->handleRequest('Options', null, null); + break; + + case 'POST': + Horde_ActiveSync::activeSyncHeader(); + + // Do the actual request + $this->_logger->debug('Horde_Rpc_ActiveSync::getResponse() starting for ' . $this->_get['Cmd']); + if (!$this->_server->handleRequest($this->_get['Cmd'], $this->_get['DeviceId'], $this->_protocolVersion)) { + /* @TODO If request failed, try to output a reasonable error to the + * device if we can...and this should be done from the ActiveSync + * objects anyway... + */ + if(!headers_sent()) { + header('Content-type: text/html'); + echo("\n"); + echo("

Error

\n"); + echo("There was a problem processing the {$this->_get['Cmd']} command from your PDA.\n"); + echo("\n"); + } + } + break; + + case 'GET': + /* Someone trying to access the activesync url from a browser */ + throw new Horde_Rpc_Exception('Trying to access the ActiveSync endpoint from a browser. Not Supported.'); + break; + } + } + + /** + * + * @see framework/Rpc/lib/Horde/Horde_Rpc#sendOutput($output) + */ + public function sendOutput($output) + { + // Unfortunately, even though zpush can stream the data to the client + // with a chunked encoding, using chunked encoding also breaks the + // progress bar on the PDA. So we de-chunk here and just output a + // content-length header and send it as a 'normal' packet. If the output + // packet exceeds 1MB (see ob_start) then it will be sent as a chunked + // packet anyway because PHP will have to flush the buffer. + $len = ob_get_length(); + $data = ob_get_contents(); + ob_end_clean(); + + // TODO: Figure this out...Z-Push had two possible paths for outputting + // to the client, 1) if the ob reached it's capacity, and here...but + // it didn't originally output the Content-Type header + header('Content-Type: application/vnd.ms-sync.wbxml'); + header('Content-Length: ' . $len); + echo $data; + } + + /** + * Check authentication. Different backends may handle + * authentication in different ways. The base class implementation + * checks for HTTP Authentication against the Horde auth setup. + * + * @TODO should the realm be configurable - since Horde is only one of the + * possible backends? + * + * @return boolean Returns true if authentication is successful. + * Should send appropriate "not authorized" headers + * or other response codes/body if auth fails, + * and take care of exiting. + */ + public function authorize() + { + $this->_logger->debug('Horde_Rpc_ActiveSync::authorize() starting'); + if (!$this->_requireAuthorization) { + return true; + } + + /* Get user and possibly domain */ + $user = $this->_request->getServer('PHP_AUTH_USER'); + $pos = strrpos($user, '\\'); + if ($pos !== false) { + $domain = substr($user, 0, $pos); + $user = substr($user, $pos + 1); + } else { + $domain = null; + } + + /* Get passwd */ + $pass = $this->_request->getServer('PHP_AUTH_PW'); + + /* Attempt to auth to backend */ + $results = $this->_backend->logon($user, $pass, $domain); + if (!$results && empty($this->_policykey)) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Basic realm="Horde RPC"'); + echo 'Access denied. Username or password incorrect.'; + // @TODO: Logging, once Rpc gets an actual logger. + } + + /* Successfully authenticated to backend, try to setup the backend */ + if (empty($this->_get['User'])) { + return false; + } + $results = $this->_backend->setup($this->_get['User']); + if (!$results) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Basic realm="Horde RPC"'); + echo 'Access denied or user ' . $this->_get['User'] . ' unknown.'; + } + + /* Policies / Provisioning */ + if ($this->_provisioning !== false && $this->_request->getServer('REQUEST_METHOD') != 'OPTIONS' && + $this->_get['Cmd'] != 'Ping' && $this->_get['Cmd'] != 'Provision' && + $this->_backend->CheckPolicy($this->_policykey, $this->_get['DeviceId']) != SYNC_PROVISION_STATUS_SUCCESS && + ($this->_provisioning !== 'loose' || + ($this->_provisioning === 'loose' && !empty($this->_policyKey)))) { + + Horde_ActiveSync::provisioningRequired(); + + return false; + } + + $this->_logger->debug('Horde_Rpc_ActiveSync::authorize() exiting'); + + return true; + } + +} diff --git a/framework/Rpc/lib/Horde/Rpc/Jsonrpc.php b/framework/Rpc/lib/Horde/Rpc/Jsonrpc.php index 7e5b550b2..c5d170319 100644 --- a/framework/Rpc/lib/Horde/Rpc/Jsonrpc.php +++ b/framework/Rpc/lib/Horde/Rpc/Jsonrpc.php @@ -31,9 +31,9 @@ class Horde_Rpc_Jsonrpc extends Horde_Rpc * @param array $config A hash containing any additional configuration or * connection parameters this class might need. */ - function __construct($params = array()) + function __construct($request, $params = array()) { - parent::__construct($params); + parent::__construct($request, $params); Horde_Nls::setCharsetEnvironment('UTF-8'); } diff --git a/framework/Rpc/lib/Horde/Rpc/Phpgw.php b/framework/Rpc/lib/Horde/Rpc/Phpgw.php index 1b8d2886e..85da702db 100644 --- a/framework/Rpc/lib/Horde/Rpc/Phpgw.php +++ b/framework/Rpc/lib/Horde/Rpc/Phpgw.php @@ -23,9 +23,9 @@ class Horde_Rpc_Phpgw extends Horde_Rpc /** * XMLRPC server constructor. */ - function __construct() + function __construct($request, $params = array()) { - parent::__construct(); + parent::__construct($request, $params); $this->_server = xmlrpc_server_create(); diff --git a/framework/Rpc/lib/Horde/Rpc/Soap.php b/framework/Rpc/lib/Horde/Rpc/Soap.php index 35ab32f63..904ec54c8 100644 --- a/framework/Rpc/lib/Horde/Rpc/Soap.php +++ b/framework/Rpc/lib/Horde/Rpc/Soap.php @@ -46,11 +46,11 @@ class Horde_Rpc_Soap extends Horde_Rpc * * @access private */ - public function __construct($params = array()) + public function __construct($request, $params = array()) { Horde_Nls::setCharset('UTF-8'); - parent::__construct($params); + parent::__construct($request, $params); if (!empty($params['allowedTypes'])) { $this->_allowedTypes = $params['allowedTypes']; diff --git a/framework/Rpc/lib/Horde/Rpc/Webdav.php b/framework/Rpc/lib/Horde/Rpc/Webdav.php index 14d87cc62..6c7dc13c1 100644 --- a/framework/Rpc/lib/Horde/Rpc/Webdav.php +++ b/framework/Rpc/lib/Horde/Rpc/Webdav.php @@ -216,7 +216,7 @@ class Horde_Rpc_Webdav extends Horde_Rpc * * @access private */ - public function __construct() + public function __construct($request, $params = array()) { // PHP messages destroy XML output -> switch them off ini_set('display_errors', 0); @@ -225,7 +225,7 @@ class Horde_Rpc_Webdav extends Horde_Rpc // so that derived classes can simply modify these $this->_SERVER = $_SERVER; - parent::__construct(); + parent::__construct($request, $params); } /** @@ -2381,11 +2381,11 @@ class Horde_Rpc_Webdav extends Horde_Rpc { $args = func_get_args(); if (count($args) == 3) { - return array("ns" => $args[0], + return array("ns" => $args[0], "name" => $args[1], "val" => $args[2]); } else { - return array("ns" => "DAV:", + return array("ns" => "DAV:", "name" => $args[0], "val" => $args[1]); } diff --git a/framework/Rpc/lib/Horde/Rpc/Xmlrpc.php b/framework/Rpc/lib/Horde/Rpc/Xmlrpc.php index cfa73ccc9..3105c2757 100644 --- a/framework/Rpc/lib/Horde/Rpc/Xmlrpc.php +++ b/framework/Rpc/lib/Horde/Rpc/Xmlrpc.php @@ -25,9 +25,9 @@ class Horde_Rpc_Xmlrpc extends Horde_Rpc * * @access private */ - public function __construct() + public function __construct($request, $params = array()) { - parent::__construct(); + parent::__construct($request, $params); $this->_server = xmlrpc_server_create(); diff --git a/framework/Rpc/package.xml b/framework/Rpc/package.xml index 7517c755e..a7341945e 100644 --- a/framework/Rpc/package.xml +++ b/framework/Rpc/package.xml @@ -49,6 +49,7 @@ various remote methods of accessing Horde functionality. + @@ -86,6 +87,7 @@ various remote methods of accessing Horde functionality. + diff --git a/framework/iCalendar/iCalendar/vcard.php b/framework/iCalendar/iCalendar/vcard.php index bc015865d..ef7f284bd 100644 --- a/framework/iCalendar/iCalendar/vcard.php +++ b/framework/iCalendar/iCalendar/vcard.php @@ -125,7 +125,7 @@ class Horde_iCalendar_vcard extends Horde_iCalendar { * * @return string The RFC822-formatted email address. */ - function getBareEmail($address) + static function getBareEmail($address) { // Empty values are still empty. if (!$address) { diff --git a/horde/config/conf.xml b/horde/config/conf.xml index 852435f99..e413e9fb0 100644 --- a/horde/config/conf.xml +++ b/horde/config/conf.xml @@ -1965,4 +1965,17 @@ + + + false + + + + State Storage Settings + /tmp + + + + + diff --git a/horde/rpc.php b/horde/rpc.php index c978bf63f..a0ad65e8c 100644 --- a/horde/rpc.php +++ b/horde/rpc.php @@ -23,23 +23,52 @@ $input = $session_control = null; $nocompress = false; $params = array(); +/* Load base libraries. */ +Horde_Registry::appInit('horde', array('authentication' => 'none', 'nocompress' => $nocompress, 'session_control' => $session_control)); + +/* Get a request object. */ +$request = new Horde_Controller_Request_Http(); + +/* TODO: This is for debugging, replace with logger from injector before merge */ +$params['logger'] = new Horde_Log_Logger(new Horde_Log_Handler_Stream(fopen('/tmp/activesync.txt', 'a'))); + /* Look at the Content-type of the request, if it is available, to try * and determine what kind of request this is. */ -if (!empty($_SERVER['PATH_INFO']) || - in_array($_SERVER['REQUEST_METHOD'], array('DELETE', 'PROPFIND', 'PUT', 'OPTIONS'))) { +if (!empty($GLOBALS['conf']['activesync']['enabled']) && + ((strpos($request->getServer('CONTENT_TYPE'), 'application/vnd.ms-sync.wbxml') !== false) || + (strpos($request->getUri(), 'Microsoft-Server-ActiveSync') !== false))) { + /* ActiveSync Request */ + $serverType = 'ActiveSync'; + $horde_session_control = 'none'; + $horde_no_compress = true; + + /* TODO: Probably want to bind a factory to injector for this? */ + $params['registry'] = $GLOBALS['registry']; + $connector = new Horde_ActiveSync_Driver_Horde_Connector_Registry($params); + $stateMachine = new Horde_ActiveSync_State_File(array('stateDir' => $GLOBALS['conf']['activesync']['state']['directory'])); + $params['backend'] = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector, + 'state_basic' => $stateMachine)); + $params['server'] = new Horde_ActiveSync($params['backend'], + new Horde_ActiveSync_Wbxml_Decoder(fopen('php://input', 'r'), Horde_ActiveSync::$zpushdtd), + new Horde_ActiveSync_Wbxml_Encoder(fopen('php://output', 'w+'), Horde_ActiveSync::$zpushdtd), + $request); + $params['server']->setLogger($params['logger']); + +} elseif ($request->getServer('PATH_INFO') || + in_array($request->getServer('REQUEST_METHOD'), array('DELETE', 'PROPFIND', 'PUT', 'OPTIONS'))) { $serverType = 'Webdav'; -} elseif (!empty($_SERVER['CONTENT_TYPE'])) { - if (strpos($_SERVER['CONTENT_TYPE'], 'application/vnd.syncml+xml') !== false) { +} elseif ($request->getServer('CONTENT_TYPE')) { + if (strpos($request->getServer('CONTENT_TYPE'), 'application/vnd.syncml+xml') !== false) { $serverType = 'Syncml'; /* Syncml does its own session handling. */ $session_control = 'none'; $nocompress = true; - } elseif (strpos($_SERVER['CONTENT_TYPE'], 'application/vnd.syncml+wbxml') !== false) { + } elseif (strpos($request->getServer('CONTENT_TYPE'), 'application/vnd.syncml+wbxml') !== false) { $serverType = 'Syncml_Wbxml'; /* Syncml does its own session handling. */ - $session_control = 'none'; - $nocompress = true; - } elseif (strpos($_SERVER['CONTENT_TYPE'], 'text/xml') !== false) { + $horde_session_control = 'none'; + $horde_no_compress = true; + } elseif (strpos($request->getServer('CONTENT_TYPE'), 'text/xml') !== false) { $input = Horde_Rpc::getInput(); /* Check for SOAP namespace URI. */ if (strpos($input, 'http://schemas.xmlsoap.org/soap/envelope/') !== false) { @@ -47,21 +76,21 @@ if (!empty($_SERVER['PATH_INFO']) || } else { $serverType = 'Xmlrpc'; } - } elseif (strpos($_SERVER['CONTENT_TYPE'], 'application/json') !== false) { + } elseif (strpos($request->getServer('CONTENT_TYPE'), 'application/json') !== false) { $serverType = 'Jsonrpc'; } else { header('HTTP/1.0 501 Not Implemented'); exit; } -} elseif (!empty($_SERVER['QUERY_STRING']) && $_SERVER['QUERY_STRING'] == 'phpgw') { +} elseif ($request->getServer('QUERY_STRING') && $request->getServer('QUERY_STRING') == 'phpgw') { $serverType = 'Phpgw'; } else { $serverType = 'Soap'; } if ($serverType == 'Soap' && - (!isset($_SERVER['REQUEST_METHOD']) || - $_SERVER['REQUEST_METHOD'] != 'POST')) { + (!$request->getServer('REQUEST_METHOD') || + $request->getServer('REQUEST_METHOD') != 'POST')) { $session_control = 'none'; $params['requireAuthorization'] = false; if (Horde_Util::getGet('wsdl') !== null) { @@ -77,11 +106,8 @@ if (($ra = Horde_Util::getGet('requestMissingAuthorization')) !== null) { $params['requestMissingAuthorization'] = $ra; } -/* Load base libraries. */ -Horde_Registry::appInit('horde', array('authentication' => 'none', 'nocompress' => $nocompress, 'session_control' => $session_control)); - /* Load the RPC backend based on $serverType. */ -$server = Horde_Rpc::factory($serverType, $params); +$server = Horde_Rpc::factory($serverType, $request, $params); /* Let the backend check authentication. By default, we look for HTTP * basic authentication against Horde, but backends can override this @@ -101,8 +127,6 @@ if (is_a($out, 'PEAR_Error')) { exit; } -/* Return the response to the client. */ -header('Content-Type: ' . $server->getResponseContentType()); -header('Content-length: ' . strlen($out)); -header('Accept-Charset: UTF-8'); -echo $out; +// Allow backends to determine how and when to send output. +$server->sendOutput($out); + diff --git a/kronolith/edit.php b/kronolith/edit.php index 01e77f1fe..ec6e192e1 100644 --- a/kronolith/edit.php +++ b/kronolith/edit.php @@ -87,12 +87,14 @@ if ($exception = Horde_Util::getFormData('del_exception')) { $exception->month, $exception->mday); $event->save(); + $uid = $event->uid; /* Create one-time event. */ $kronolith_driver->open($target); $event = $kronolith_driver->getEvent(); $event->readForm(); $event->recurrence->setRecurType(Horde_Date_Recurrence::RECUR_NONE); + $event->baseid = $uid; break; diff --git a/kronolith/lib/Api.php b/kronolith/lib/Api.php index 5467d8e01..c13b29bd8 100644 --- a/kronolith/lib/Api.php +++ b/kronolith/lib/Api.php @@ -552,6 +552,8 @@ class Kronolith_Api extends Horde_Registry_Api throw new Horde_Exception_PermissionDenied(); } + $kronolith_driver = Kronolith::getDriver(null, $calendar); + switch ($contentType) { case 'text/calendar': case 'text/x-vcalendar': @@ -569,7 +571,6 @@ class Kronolith_Api extends Horde_Registry_Api throw new Kronolith_Exception(_("No iCalendar data was found.")); } - $kronolith_driver = Kronolith::getDriver(null, $calendar); $ids = array(); foreach ($components as $content) { if ($content instanceof Horde_iCalendar_vevent) { @@ -608,6 +609,12 @@ class Kronolith_Api extends Horde_Registry_Api return $ids[0]; } return $ids; + + case 'activesync': + $event = $kronolith_driver->getEvent(); + $event->fromASAppointment($content); + $event->save(); + return $event->uid; } throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType)); @@ -679,6 +686,8 @@ class Kronolith_Api extends Horde_Registry_Api return $iCal->exportvCalendar(); + case 'activesync': + return $event->toASAppointment(); } throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType)); @@ -820,6 +829,11 @@ class Kronolith_Api extends Horde_Registry_Api if ($content instanceof Horde_iCalendar_vevent) { $component = $content; + } elseif ($content instanceof Horde_ActiveSync_Message_Appointment) { + $event->fromASAppointment($content); + $event->save(); + $event->uid = $uid; + return; } else { switch ($contentType) { case 'text/calendar': @@ -1000,12 +1014,16 @@ class Kronolith_Api extends Horde_Registry_Api * @param boolean $showRemote Return events from remote calendars and * listTimeObject API as well? * + * @param boolean $hideExceptions Hide events that represent exceptions to + * a recurring event (events with baseid + * set)? + * * @return array A list of event hashes. * @throws Kronolith_Exception */ public function listEvents($startstamp = null, $endstamp = null, $calendars = null, $showRecurrence = true, - $alarmsOnly = false, $showRemote = true) + $alarmsOnly = false, $showRemote = true, $hideExceptions = false) { if (!isset($calendars)) { $calendars = array($GLOBALS['prefs']->getValue('default_share')); @@ -1019,9 +1037,14 @@ class Kronolith_Api extends Horde_Registry_Api } } - return Kronolith::listEvents(new Horde_Date($startstamp), + return Kronolith::listEvents( + new Horde_Date($startstamp), new Horde_Date($endstamp), - $calendars, $showRecurrence, $alarmsOnly, $showRemote); + $calendars, + $showRecurrence, + $alarmsOnly, + $showRemote, + $hideExceptions); } /** diff --git a/kronolith/lib/Driver/Sql.php b/kronolith/lib/Driver/Sql.php index dbe82cf5e..6ad387fe5 100644 --- a/kronolith/lib/Driver/Sql.php +++ b/kronolith/lib/Driver/Sql.php @@ -146,6 +146,17 @@ class Kronolith_Driver_Sql extends Kronolith_Driver } } } + + if (!empty($query->baseid)) { + $binds = Horde_SQL::buildClause($this->_db, 'event_baseid', '=', $query->baseid, true); + if (is_array($binds)) { + $cond .= $binds[0] . ' AND '; + $values = array_merge($values, $binds[1]); + } else { + $cond .= $binds; + } + } + if (isset($query->status)) { $binds = Horde_SQL::buildClause($this->_db, 'event_status', '=', $query->status, true); if (is_array($binds)) { @@ -266,7 +277,7 @@ class Kronolith_Driver_Sql extends Kronolith_Driver */ public function listEvents($startDate = null, $endDate = null, $showRecurrence = false, $hasAlarm = false, - $json = false, $coverDates = true) + $json = false, $coverDates = true, $hideExceptions = false) { if (!is_null($startDate)) { $startDate = clone $startDate; @@ -278,9 +289,16 @@ class Kronolith_Driver_Sql extends Kronolith_Driver $endDate->min = $endDate->sec = 59; } - $events = $this->_listEventsConditional($startDate, $endDate, - $hasAlarm ? 'event_alarm > ?' : '', - $hasAlarm ? array(0) : array()); + $conditions = $hasAlarm ? 'event_alarm > ?' : ''; + $values = $hasAlarm ? array(0) : array(); + if ($hideExceptions) { + if (!empty($conditions)) { + $conditions .= ' AND '; + } + $conditions .= "event_baseid = ''"; + } + + $events = $this->_listEventsConditional($startDate, $endDate, $conditions, $values); $results = array(); foreach ($events as $id) { Kronolith::addEvents($results, $this->getEvent($id), $startDate, @@ -312,7 +330,7 @@ class Kronolith_Driver_Sql extends Kronolith_Driver ' event_recurtype, event_recurenddate, event_recurinterval,' . ' event_recurdays, event_start, event_end, event_allday,' . ' event_alarm, event_alarm_methods, event_modified,' . - ' event_exceptions, event_creator_id, event_resources' . + ' event_exceptions, event_creator_id, event_resources, event_baseid' . ' FROM ' . $this->_params['table'] . ' WHERE calendar_id = ?'; $values = array($this->calendar); @@ -426,8 +444,9 @@ class Kronolith_Driver_Sql extends Kronolith_Driver ' event_recurtype, event_recurenddate, event_recurinterval,' . ' event_recurdays, event_start, event_end, event_allday,' . ' event_alarm, event_alarm_methods, event_modified,' . - ' event_exceptions, event_creator_id, event_resources' . - ' FROM ' . $this->_params['table'] . ' WHERE event_id = ? AND calendar_id = ?'; + ' event_exceptions, event_creator_id, event_resources,' . + ' event_baseid FROM ' . $this->_params['table'] . + ' WHERE event_id = ? AND calendar_id = ?'; $values = array($eventId, $this->calendar); /* Log the query at a DEBUG log level. */ diff --git a/kronolith/lib/Event.php b/kronolith/lib/Event.php index a63ff0920..0f303bee1 100644 --- a/kronolith/lib/Event.php +++ b/kronolith/lib/Event.php @@ -269,6 +269,14 @@ abstract class Kronolith_Event protected $_rowspan; /** + * The baseid. For events that represent exceptions this is the UID of the + * original, recurring event. + * + * @var string + */ + public $baseid; + + /** * Constructor. * * @param Kronolith_Driver $driver The backend driver that this event is @@ -966,6 +974,176 @@ abstract class Kronolith_Event } /** + * Imports the values for this event from a MS ActiveSync Message. + * + * @see Horde_ActiveSync_Message_Appointment + */ + public function fromASAppointment(Horde_ActiveSync_Message_Appointment $message) + { + /* New event? */ + if ($this->id === null) { + $this->creator = Horde_Auth::getAuth(); + } + if ($title = Horde_String::convertCharset($message->getSubject(), 'utf-8', Horde_Nls::getCharset())) { + $this->title = $title; + } + if ($description = Horde_String::convertCharset($message->getBody(), 'utf-8', Horde_Nls::getCharset())) { + $this->description = $description; + } + if ($location = Horde_String::convertCharset($message->getLocation(), 'utf-8', Horde_Nls::getCharset())) { + $this->location = $location; + } + + /* Date/times */ + $dates = $message->getDatetime(); + $this->start = $dates['start']; + $this->end = $dates['end']; + $this->allday = $dates['allday']; + + /* Sensitivity */ + $this->private = ($message->getSensitivity() == 'private' || $message->getSensitivity() == 'confidential') ? true : false; + + /* Response Status */ + $status = $message->getResponseType(); + switch ($status) { + case 'declined': + $status = 'CANCELLED'; + break; + case 'accepted': + $status = 'CONFIRMED'; + break; + case 'tenative': + $status = 'TENATIVE'; + default: + $status = 'FREE'; + } + $this->status = constant('Kronolith::STATUS_' . $status); + + /* Alarm */ + if ($alarm = $message->getReminder()) { + $this->alarm = $alarm; + } + + /* Attendees */ + + /* Recurrence */ + if ($rrule = $message->getRecurrence()) { + $this->recurrence = $rrule; + + /* Exceptions */ + /* Since AS keeps exceptions as part of the original event, we need to + * delete all existing exceptions and re-create them. The only drawback + * to this is that the UIDs will change. + */ + if (!empty($this->uid)) { + $kronolith_driver = Kronolith::getDriver(null, $this->calendar); + $search = new StdClass(); + $search->start = $rrule->getRecurStart(); + $search->end = $rrule->getRecurEnd(); + $search->baseid = $this->uid; + $results = $kronolith_driver->search($search); + foreach ($results as $days) { + foreach ($days as $exception) { + $kronolith_driver->deleteEvent($exception->id); + } + } + } + + $erules = $message->getExceptions(); + foreach ($erules as $rule){ + $d = new Horde_Date($rule->getExceptionStartTime()); + $this->recurrence->addException($d->format('Y'), $d->format('m'), $d->format('d')); + + /* Readd the exception event */ + $event = $kronolith_driver->getEvent(); + $times = $rule->getDatetime(); + $event->start = $times['start']; + $event->end = $times['end']; + $event->allday = $times['allday']; + $event->title = Horde_String::convertCharset($rule->getSubject(), 'utf-8', Horde_Nls::getCharset()); + $event->description = Horde_String::convertCharset($rule->getBody(), 'utf-8', Horde_Nls::getCharset()); + $event->baseid = $this->uid; + $event->initialized = true; + $event->save(); + } + } + + /* Flag that we are initialized */ + $this->initialized = true; + } + + /** + * Export this event as a MS ActiveSync Message + * + * @return Horde_ActiveSync_Message_Appointment + */ + public function toASAppointment() + { + $message = new Horde_ActiveSync_Message_Appointment(array('logger' => $GLOBALS['injector']->getInstance('Horde_Log_Logger'))); + $message->setSubject(Horde_String::convertCharset($this->getTitle(), Horde_Nls::getCharset(), 'utf-8')); + $message->setBody(Horde_String::convertCharset($this->description, Horde_Nls::getCharset(), 'utf-8')); + $message->setLocation(Horde_String::convertCharset($this->location, Horde_Nls::getCharset(), 'utf-8')); + + /* Start and End */ + $message->setDatetime(array('start' => $this->start, + 'end' => $this->end, + 'allday' => $this->isAllDay())); + + /* Timezone */ + $message->setTimezone($this->start); + + /* Organizer */ + $name = Kronolith::getUserName($this->creator); + $name = Horde_String::convertCharset($name, Horde_Nls::getCharset(), 'utf-8'); + $message->setOrganizer( + array('name' => $name, + 'email' => Kronolith::getUserEmail($this->creator)) + ); + + /* Privacy */ + $message->setSensitivity($this->private ? 'private' : 'normal'); + + /* Response Status */ + switch ($this->status) { + case Kronolith::STATUS_CANCELLED: + $status = 'declined'; + break; + case Kronolith::STATUS_CONFIRMED: + $status = 'accepted'; + break; + case Kronolith::STATUS_TENTATIVE: + $status = 'tenative'; + case Kronolith::STATUS_FREE: + case Kronolith::STATUS_NONE: + $status = 'none'; + } + $message->setResponseType($status); + + /* DTStamp */ + $message->setDTStamp($_SERVER['REQUEST_TIME']); + + /* Recurrence */ + if ($this->recurs()) { + $message->setRecurrence($this->recurrence); + + /* Exceptions */ + if (!empty($this->recurrence) && $exceptions = $this->recurrence->getExceptions()) { + foreach ($exceptions as $start) { + $e = new Horde_ActiveSync_Message_Exception(); + $e->setDateTime(array('start' => new Horde_Date($start))); + $message->addException($e); + } + } + } + + /* Attendees */ + + $message->setReminder($this->alarm); + + return $message; + } + + /** * Imports the values for this event from an array of values. * * @param array $hash Array containing all the values. diff --git a/kronolith/lib/Event/Sql.php b/kronolith/lib/Event/Sql.php index db7add170..c8b339b42 100644 --- a/kronolith/lib/Event/Sql.php +++ b/kronolith/lib/Event/Sql.php @@ -137,6 +137,10 @@ class Kronolith_Event_Sql extends Kronolith_Event if (isset($SQLEvent['event_alarm_methods'])) { $this->methods = $driver->convertFromDriver(unserialize($SQLEvent['event_alarm_methods'])); } + if (isset($SQLEvent['event_baseid'])) { + $this->baseid = $SQLEvent['event_baseid']; + } + $this->initialized = true; $this->stored = true; } @@ -210,6 +214,10 @@ class Kronolith_Event_Sql extends Kronolith_Event } $this->_properties['event_exceptions'] = implode(',', $this->recurrence->getExceptions()); } + + if (!empty($this->baseid)) { + $this->_properties['event_baseid'] = $this->baseid; + } } public function getProperties() diff --git a/kronolith/lib/Kronolith.php b/kronolith/lib/Kronolith.php index 937e2dd24..917094869 100644 --- a/kronolith/lib/Kronolith.php +++ b/kronolith/lib/Kronolith.php @@ -454,13 +454,16 @@ class Kronolith * Defaults to false * @param boolean $showRemote Return events from remote and * listTimeObjects as well? + * @param boolean $hideExceptions Hide events that represent exceptions to + * a recurring event? * * @return array The events happening in this time period. * @throws Kronolith_Exception */ public static function listEvents($startDate, $endDate, $calendars = null, $showRecurrence = true, - $alarmsOnly = false, $showRemote = true) + $alarmsOnly = false, $showRemote = true, + $hideExceptions = false) { $results = array(); @@ -471,7 +474,10 @@ class Kronolith $driver = self::getDriver(); foreach ($calendars as $calendar) { $driver->open($calendar); - $events = $driver->listEvents($startDate, $endDate, $showRecurrence); + $events = $driver->listEvents($startDate, $endDate, $showRecurrence, + $alarmsOnly, false, true, + $hideExceptions); + self::mergeEvents($results, $events); } diff --git a/kronolith/scripts/sql/kronolith.mssql.sql b/kronolith/scripts/sql/kronolith.mssql.sql index 83a5c33c5..06b59bd93 100644 --- a/kronolith/scripts/sql/kronolith.mssql.sql +++ b/kronolith/scripts/sql/kronolith.mssql.sql @@ -23,6 +23,7 @@ CREATE TABLE kronolith_events ( event_alarm_methods VARCHAR(MAX), event_modified INT NOT NULL, event_private INT DEFAULT 0 NOT NULL, + event_baseid VARCHAR(255) DEFAULT '', PRIMARY KEY (event_id) ); diff --git a/kronolith/scripts/sql/kronolith.mysql.sql b/kronolith/scripts/sql/kronolith.mysql.sql index 8055d59b5..70ca897cf 100644 --- a/kronolith/scripts/sql/kronolith.mysql.sql +++ b/kronolith/scripts/sql/kronolith.mysql.sql @@ -23,6 +23,7 @@ CREATE TABLE kronolith_events ( event_alarm_methods TEXT, event_modified INT NOT NULL, event_private TINYINT DEFAULT 0 NOT NULL, + event_baseid VARCHAR(255) DEFAULT '', PRIMARY KEY (event_id) ); diff --git a/kronolith/scripts/sql/kronolith.oci8.sql b/kronolith/scripts/sql/kronolith.oci8.sql index 3c5c4a6c3..c904ba9ea 100644 --- a/kronolith/scripts/sql/kronolith.oci8.sql +++ b/kronolith/scripts/sql/kronolith.oci8.sql @@ -23,6 +23,8 @@ CREATE TABLE kronolith_events ( event_alarm_methods VARCHAR2(4000), event_modified NUMBER(16) NOT NULL, event_private NUMBER(1) DEFAULT 0 NOT NULL, + event_baseid VARCHAR2(255) DEFAULT '', + -- PRIMARY KEY (event_id) ); diff --git a/kronolith/scripts/sql/kronolith.pgsql.sql b/kronolith/scripts/sql/kronolith.pgsql.sql index 130347d2e..2daa72ac0 100644 --- a/kronolith/scripts/sql/kronolith.pgsql.sql +++ b/kronolith/scripts/sql/kronolith.pgsql.sql @@ -23,6 +23,7 @@ CREATE TABLE kronolith_events ( event_alarm_methods TEXT, event_modified INT NOT NULL, event_private INT DEFAULT 0 NOT NULL, + event_baseid VARCHAR(255) DEFAULT '', PRIMARY KEY (event_id) ); diff --git a/kronolith/scripts/sql/kronolith.sql b/kronolith/scripts/sql/kronolith.sql index 23c3c6809..d76c10fc4 100644 --- a/kronolith/scripts/sql/kronolith.sql +++ b/kronolith/scripts/sql/kronolith.sql @@ -23,6 +23,7 @@ CREATE TABLE kronolith_events ( event_alarm_methods TEXT, event_modified INT NOT NULL, event_private INT DEFAULT 0 NOT NULL, + event_baseid VARCHAR(255) DEFAULT '', PRIMARY KEY (event_id) ); diff --git a/kronolith/scripts/sql/kronolith.xml b/kronolith/scripts/sql/kronolith.xml index 7de24113d..f402a9f02 100644 --- a/kronolith/scripts/sql/kronolith.xml +++ b/kronolith/scripts/sql/kronolith.xml @@ -152,6 +152,12 @@ 0 + + event_baseid + text + 255 + + kronolith_primary true diff --git a/kronolith/scripts/upgrades/2010-03-25_add_baseid.sql b/kronolith/scripts/upgrades/2010-03-25_add_baseid.sql new file mode 100644 index 000000000..831fd068b --- /dev/null +++ b/kronolith/scripts/upgrades/2010-03-25_add_baseid.sql @@ -0,0 +1 @@ +ALTER TABLE kronolith_events ADD event_baseid VARCHAR(255) DEFAULT ''; diff --git a/turba/lib/Api.php b/turba/lib/Api.php index 236422886..c99002747 100644 --- a/turba/lib/Api.php +++ b/turba/lib/Api.php @@ -727,7 +727,8 @@ class Turba_Api extends Horde_Registry_Api * text/vcard - text/x-vcard The first two * produce a vcard3.0 (rfc2426), the second * produces a vcard in old 2.1 format - * defined by imc.org + * defined by imc.org Also supports a raw + * array * @param string|array $sources The source(s) from which the contact will * be exported. * @param array $fields Hash of field names and SyncML_Property @@ -780,6 +781,7 @@ class Turba_Api extends Horde_Registry_Api throw new Horde_Exception("Internal Horde Error: multiple turba objects with same objectId."); } + $version = '3.0'; list($contentType,) = explode(';', $contentType); switch ($contentType) { @@ -797,6 +799,16 @@ class Turba_Api extends Horde_Registry_Api $export .= $vcard->exportvCalendar(); } return $export; + + case 'array': + $attributes = array(); + foreach ($result->objects as $object) { + foreach ($cfgSources[$source]['map'] as $field => $map) { + $attributes[$field] = $object->getValue($field); + } + } + + return $attributes; } throw new Horde_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType)); -- 2.11.0