diff --git a/egwical/doc/EgwIcal-BUGS-and-TODOS.txt b/egwical/doc/EgwIcal-BUGS-and-TODOS.txt new file mode 100644 index 0000000000..1cd51ba64d --- /dev/null +++ b/egwical/doc/EgwIcal-BUGS-and-TODOS.txt @@ -0,0 +1,95 @@ +/*! \page pageegwicalbugandtodo BUGS and TODOS + +
+Know BUGS and things todo
+---------------------------
+TODO means: should be done/would be nice  in a newer release
+BUG means: known problem needs to be fixed
+FAIl means: known failure to provide (not easily to be done/fixed
+
+(note: this list is probably not quite up todate)
+
+
+release: VERSION 0.9.0 NEED TO UPDATE THIS!!
+
+new in V0.7.78 versus v0.7.70:
+
+  bovevents:
+
++ whole day events import/export seems to work nice
++ continue on erroneous overwrites (errors only visible in errorlog)
++ no error messages for forbidden overwrite events of non owned events
++ errormessages (when activated) only shows problematic events
+++ basic (multiple) VALARM import and export, working! (without action select)
++ allow for vevents with either DTEND or DURATION
++ provided (horde iCalendar) patch to prevent segfaults on some bad iCal inputs
+
+1) EgwIcal package general (V0.9)
+----------------
+
+
+[ ]1.2 TODO get it nicely into egw cvs
+[+/-]1.7 TODO/WISH allow for import of ATTENDEEs not in Egw yet (create new addressbook entry?)
+         [+](ad hoc solution: add CN and mailto to description)
+[+/-]1.4 TODO check and generate source code documentation(phpdoc or doxygen)
+
+[ ]1.5 TODO test these routines in use for syncml import/export
+
+1.6 TODO/WISH: nothing done yet for vfreebusy, vjournal, notes, components 
+    only vevents and vtodos are supported.
+
+
+3) egwical.egwical (V0.9.01)
+--------------------
+
+[+?]3.1 TODO check/implement correct timezone handling (seems ok) 
+[ ]3.9 TEST with syncml 		
+[ ]3.2 TODO/WISH handle location (GEO), link(?), url, delegation, ()CONTACT?
+[ ]3.3 TODO write more documentation (continue mainpage)
+[ ]3.4 TODO add accumulting buffer system to egwical
+
+4) egwical.bocalupdate_vevents (V0.9.02)
+------------------------------
+
+[+?]4.1 TODO check/implement correct timezone handling (related 3.1)
+[ ]4.2a TODO/WISH handle URL, GEO etc. fields. (needs extra fields handling in bocalupdate!)
+[ ]4.2b FAIL rrule-COUNT import not implemented in egw?
+[+]4.2 BUG!!!!  rrule-BYDAY import goes wrong. FIXED (v0.7.76)
+[+]4.3 TODO check and improve ORGANIZER,ATTENDEE<-> participants mapping
+	     ATTENDEE s import and export (appears to work oke >= V0.7.71) 
+[+]4.4 TODO handle import and export of ALARMS
+[ ]4.7 TODO (ad hoc) add non egw known attendees to description on import
+[ ]4.8 TODO move compatibility code to compat classes.
+
+
+4.9 see 3.2
+
+
+5) egwical.boinfolog_vtodos (V0.9.02)
+----------------------------
+5.1 TODO check/implement correct timezone handling  (related 3.1)
+5.3 FAIL import/export more than 1 category per task (egw doesnot allow)
+5.3 TODO check and improve ORGANIZER,ATTENDEE<-> info_responsible etc mapping
+5.4 TODO handle import and export of ALARMS
+5.5 TODO rewrite code into supportedFields structure (for next version)
+[ ]5.9 TODO rewrite into skeleton structure analog to egwical.bocalupdate_vevents
+
+
+
+10.) in  used routines from others:
+
+10.1 BUG Horde_iCalendar(1.2rc6): EXDATE bug
+  -[+]fixed by patch 'exdate ....'
+10.2 BUG  Horde_iCalendar(1.2rc6): standard.php Warning
+  -[+]fixed by patch '...??..'
+
+10.3 BUG infolog(1.2rc6): datetime is 1 hour wrong for untimed due field in tasks
+
+11) detected errors/flaws in other programs
+
+11.1 Korganizer 3.5: recurrence endondate display is interpreted different from egw and
+mozilla (and probable also rfc 2445) 
+
+
+
+*/ \ No newline at end of file diff --git a/egwical/doc/EgwIcal-Timezone-handling.txt b/egwical/doc/EgwIcal-Timezone-handling.txt new file mode 100644 index 0000000000..d75b3c49b3 --- /dev/null +++ b/egwical/doc/EgwIcal-Timezone-handling.txt @@ -0,0 +1,183 @@ +/*! \page pageegwicaltzh EgwIcal Timezone Handling + +Short description on the timezone handling in EgwIcal + +@date 20060216 +@version 0.9.04 +@author JVL +@note Document needs further editing and annotations!! + + + +In EgwIcal the timed data of Egroupware like events and tasks get +converted to elements for an iCalendar. In the Egroupware server we +are dealing with socalled "egw server" time, in applications that a +user of egw runs, the user can set a "locale" and thereby defining a +"local timezone". And finally when events and task get exported to an +iCalendar that is sent to a client or uploaded from it, they may also +have their timesettings defined according to some "timezone". + +How is this all handled in EgwIcal? + + + +@section secegwicalgentzh EgwIcal general Timezone handling + +In general EgwIcal distinguishes between two different types of time +descriptions for an event. An event can either be a: + + + +For handling DT events it can be profitable to use a globally unified standard +for describing the timingly definitions, like e.g. UTC. +And indeed: + + + +For handling D events you can best give a (timezone/geo) relative +description together with the GEO location. Then someone who wants to +visit such a event can checkout the location and see what timezone +they have there at moment of the event, calculate it back to UTC and +then she knows when to get there. +Egw and EgwIcal are no very sophisticated in handling these: + +- RE.1 (I am not 100% sure:) Egw tries to handle DATE events relative to + the UserInterface its timezone: the so called USER-TIME. + +and for EgwIcal: + +- REI.1 EgwIcal tries to handle DATE events relative to + the UserInterface timezone (USER-TIME) of the IcalSrv proces of the + logged in user. + +and keep in mind: + +- REI.2 EgwIcal currently will only import or export DATE events + for whole day events or for occurence settings of recurrent + events + +In the following sections I will explain a bit more what this means +for import of Ical data from clients and export to it. + + + + +@section secegwicalimptzh EgwIcal Timezone handling for icaldata import from a client. + +..... + +@subsection subsecvtmz VTIMEZONE import handling. + + No VTIMEZONE ical elements are parsed or used + This has a consequence: + + - no basic import timezone setting via this supported + + - no imported timezones with identifiers that can be referenced by + VEVENTS or VTODOS are supported + +@subsection subsecdtimp DATETIME formatted data import handling. + + DATETIME formatted data is inspected for its timezone. If the + timezone is UTC ('Z') then it is converted (i.e. copied) to UTC + before processing further. If the timezone is not given of is + referenced with a key to a VTIMEZONE component in the Icalendar + file, this timezone info is discarded and a "timezoned" DATETIME + is just interpreted as relative to the USERTIME of the IcalSrv + process: any timezone info is neglected. + + Clearly this can be wrong in many occasions: thus this should be + considered a BUG! + +@subsection subsecdimp DATE formatted data import handling. + + DATE formatted data is ust interpreted as relative to the USERTIME of the IcalSrv + process: any timezone info is neglected. + + +@subsection subsecrruleimp Importing Recurrence Elements + + + + Doing recurrence calculation in UTC is notably not a good thing to + do because DayTime Savings changes during the runtime of the + recurring event can cause troubles. The best thing to do is to + calculate the recurrence in the geo/time-zone of the definer. See + @url ... of the CAlConnect Group for this. But as clients often dont + export their timezones, (and we dont do any processing of it anyhow) + we do the following: + +
    +
  1. first the START and (if there UNTIL) dates are converted to UTC + dates. Then these are converted to the IcalSrv locale setting. + +
  2. Then the recurrence rules are evaluated, and converted to the info + Egw needs. Note: Egw cannot use the COUNT field, so EgwIcal has to + rewrite it to a UNTIL value. +
+ + + +@section secegwicalexptzh EgwIcal Timezone handling for icaldata export to a client. + +On export .... + +- all Egw DATETIME info is exported in UTC time. + +- all Egw DATE info is exported without any timezone info, + and thus should best be considered in the USERTIME of the IcalSrv + process. + + +@subsection subsecrruleexp Exporting Recurrence Elements + +On export Recurrence Rules are interpreted as follows: + + + + + +Hope this explains things a bit. + +Jan + +*/ \ No newline at end of file diff --git a/egwical/doc/EgwIcal-intro.txt b/egwical/doc/EgwIcal-intro.txt new file mode 100644 index 0000000000..1f01966be9 --- /dev/null +++ b/egwical/doc/EgwIcal-intro.txt @@ -0,0 +1,182 @@ +/*! @mainpage EgwIcal package for Egroupware + +@NOTE this text is not up to date +@version 0.9.01 +@author jvl + +The EgwIcal package provides routines for converting between iCalendar RFC +2445 conformant dataelements (like VEVENTS, VTODOS, VALARMS etc.) and +corresponding Egroupware database entries (like calendar events, +infolog tasks etc.). EgwIcal also provides routines to do import to +and export from the Egroupware system for these iCalendar elements, +thereby doing the conversion on the fly. Finally, EgwIcal provides a +buffering feature where mixed sets of these elements can be accumulated and +then be imported or exported all at once. + +@section secinstall SHORT experimental Install How-To: + + - Install the EgwIcal package by either checking out the egwical module from cvs + or untarring some tarball egwical-vx.y.x.tgz of the package (when available). + + +@section secusage Usage of the EgwIcal system + +The EgwIcal system allows four(or 3?) characteristic ways to help you +with manipulating iCalendar data in a Egw context:..... + +@section secusageiecnv Converting iCalendar elements to Egw elements + +To convert a iCalendar datacomponent e.g. of type "VEVENT" into a +corresponding egw component of e.g. "calendar event" you can use the +following in your code: + + ...... + + +@section secusageeicnv Converting Egw elements to iCalendar elements. + +To convert a egw component of e.g. "calendar event" into a +corresponding iCalendar datacomponent of e.g. type "VEVENT"you can use the +following in your code: + + ...... + + +@section secusageimport Importing iCalendar elements into Egw +applications storage. + +To import a iCalendar datacomponentof e.g. type "VEVENT" as a +corresponding egw component of type "calendar event" into your +calendar application (its database) you can use the +following in your code: + + ...... + + +@section secusageexport Exporting Egw application elements as iCalendar elements. + +To export as a egw datacomponent of e.g. type "calendar event" from your +calendar application (its database) as a iCalendar datacomponentof e.g. type "VEVENT", +you can use the following in your code: + + ...... + + +@section secusageexpimpbuffer Exporting (mixed) collections of iCalendar elements. + +Beside directly exporting single elements or sets of +iCalendar data generated generated from a egw application like +calendar, you can also use the "buffer" system of EgwIcal, to collect +various sets of such dataelements, even from different Egw +applications (like calendar and infolog), and then later on export +these all together as one big iCalendar. + +To do collection followed by export you can use code like below: + ...... + + + +@section secimplementation How is EgwIcal implemented? + +To use or extend EgwIcal best, as a developer, it may be good know a bit about +its implementation. +EgwIcal is built according to the Workers Union Representatives +Hierarchy (WURH) pattern. Quite a mouthfull, and probably there are +somewhere better names for this pattern (see ...). In @ref pageegwicalwurh +you can read more on this. + +Basically it means that there is one super "worker" class +@ref egwical that uses for specific work like e.g. conversion +between VEVENTS and calendar tasks, a specific subclass +like e.g. bocalupdate_vevents that is connected to the +egwical object via a socalled "representative" member in the registry $reg_rscs +(attribute). + +Currently EgwIcal has the following subclasses: + +- bocalupdate_vevents for manipulating calendar events as + VEVENTS. + +- boinfolog_vtodos for manipulating infolog tasks as VTODOS. + +Future subclasses might be: + +- ???_vjournals for manipulating .... as VJOURNALS. + +- bocal_vfreebusies for manipulating calendar data as + VFREEBUSY elements. + +- addressbook_vcard for manipulating addressdata data as + VCARDS. + +- ???_vnotes for manipulating .... as VNOTES. + + + +@section secdeletioncheatcode Cheatcode for Deleting Calendars, events or todos + +Egwical provides a special "hackish" feature specifically meant for +use with the crippeld ical-over-http (aka webcal) implementation that +the package IcalSrv provides for Egroupware. + +This IcalSrv implementation does not support deletion of egw calendar +or infolog elements directly (see the IcalSrv package). As a help out +the "cheat" is implemented that: you can though delete an item by +simply giving the VEVENT or VTODO 'SUMMARY' field a value +X-DELETE or _DELETED_ and then importing it +through the Egwical system. I you do so the the corresponding egw +element from calendar or infolog will get deleted. + + +@section secpatches Patches + + +You may need to apply some of the patches found in PATCHES. +Currently there a two patches to fix a warning and a bug (related to EXDATE +fields handling) in the horde routines. +Note these may be already committed in the CVS version. + + + +@section sectest Extra testing + +The package contains two "compatibility" classes (@ref boical and @ref vcalinfolog) with +which you can replace +the current infolog/inc/class.vcalinfolog.inc.php resp. +calendar/inc/class.boical.inc.php +files. + +When done so, the egw syncml, ical import/export and infolog import/export in egw +will all use the new code. This way you can discover more bugs and failure in the code :) + +But, when not renamed, these programs will use the old code (and thus are not broken or +improved by the new one..) + + +@section secdocumentation Documentation + +There is (somewhere) complete doxygen generated documentation for the +EgwIcal package. Otherwise you can generate it yourself by using +doxygen on the source filetree. Maybe phpdocumenter can give you also +something usefull. And maybe you are just reading it now this very moment... + + + +@section secbugandtodo BuG and todo list + + @todo notably timezone handling in EgwIcal should be documented and +possible features to allow import/export and use of iCalendar +VTIMEZONE components should be provided. + + -see BUGs and TODOs in @ref pageegwicalbugandtodo + + + + +have fun + + +JVL + + +*/ \ No newline at end of file diff --git a/egwical/doc/egwical-doxydoc.tgz b/egwical/doc/egwical-doxydoc.tgz new file mode 100644 index 0000000000..a3c46fdf8b Binary files /dev/null and b/egwical/doc/egwical-doxydoc.tgz differ diff --git a/egwical/doc/egwical-wurh-pattern.txt b/egwical/doc/egwical-wurh-pattern.txt new file mode 100644 index 0000000000..b33329a01c --- /dev/null +++ b/egwical/doc/egwical-wurh-pattern.txt @@ -0,0 +1,205 @@ +/*! +\page pageegwicalwurh EgwIcal Workers Union Representatives Hierarchy Pattern +implementation. + +@author JVL +@date 20060214 +@version v0.9.02 + +The current Egw Ical package provides routines for exporting iCalendar +data like vevents, vtodos, vcards etc. from the various corresponding +Egroupware packages (the "backends"). And vice versa importing these to the datastores +of these packages. On the other hand there can also be a variety of +interface components that transport e.g these exported vevents to +further applications (the "clients"). Think e.g on the "icalsrv" +service to transport whole sets of vevents in one go over http to a +client, or the "syncml" service that transports them to syncml +speaking device, or simple routine that let you save or upload a +calendar file from within the webgui. + +An iCalendar data object can hold a set of these various kinds of +data-elements as parts of itself. And these data-elements itself can +also be composed of sub-elements (like e.g. VALARM and VTIMEZONE +components). + + +So in this situation we have Wholes (like an iCalendar object) with +Parts (like groups of Vevents and groups of Vtodos) on the one hand. +And on the other hand we have dedicated subappliations that may handle +these groups of Part data. To implement this we can make use of a +smart combination of two implementation primitives "compounds" and +"classes and subclasses". + +In the WURH pattern the combination of these two is very rather +outspoken and quite entangled and even a bit tricky: each specific +type of Part is "represented" by specific subclass of the class of +Whole compound. The data in the parts can now be manipulated in two +ways: + +1) all together as whole, via methods of the compound, or +2) all in the group of a part, by the methods of the (associated) +subclass its representative. + +@note I forgot the official (GOF) name of the pattern (if there is +any..) + + + +\section secwurhexample Example WURH pattern implementation: Meal preparation + +As simple example should explain this: Meal preparation URH pattern +implementation + +think of the preparation of meal, that consist of soup and meat. +To "cook" the meal, the soup has to be boiled and the meat has to be +baked. + +in OO notation: <>- or . . means member of, +<- means subclass of + +Classes: +
+MealCooker <- SoupCooker;     MealCooker <- MeatCooker;
+MealCooker <>- SoupCooker;    MealCooker <>- MeatCooker;
+
+Methods: +
+MealCooker->cook();  SoupCooker->boil();   MeatCooker->bake();
+
+and possibly the cook() method may even, for easy of use in the Soup and +Meat classes by implemented (overridden) by the boil() resp. bake() method. + +Now the code to prepare to have $mymealck prepare a meail with $mysoupck and $mymeatck (without +subclass overrriding): +
+$mymealck = New MealCooker;
+$mymeatck = New MeatCooker;
+$mysoupck = New SoupCooker;
+
+$meat_in_mealck =  $mymealck->addMeatHandler($mymeatck);  
+$soup_in mealck =  $mymealck->addSoupHandler($mysoupck);
+
+// these are in class MealCooker implemented as
+// $this.m = meatcooker_config($mymeatck); resp. $this.s = soupcooker_config($mysoupck); 
+
+ +Now cooking the meal is done by, cooking the soup in its appropiate +way (namely "boiling()" )via its union representative ($soup_in_mealck). Note that it is +called "union" representative because multiple SoupCookers parts may have been +added already to use. And also cooking the meat in its appropiate way +("baking()") likewise. Thus +
+$mymealck->cook([$apiec_of_meat,$avolum_of_soup]) can be done by calling:
+
+		$meat_in_mealck->bake($apiece_of_meat);
+		$soup_in_mealck->boil($avolume_of_soup);
+
+and then you can serve boths parts in one go with: +
+$mymealck->serve();
+
+ +If we implemented the specific ways of cooking as overriding subclass +methods for Meal->cook() then in simple cases we dont even need to use +the representatives to call the preparation as we implement to main +cook() method to call all itself on all its parts: +
+$mymealck = New MealCooker;
+$mymeatck = New MeatCooker;
+$mysoupck = New SoupCooker;
+
+		  $mymealck->addMeatHandler($mymeatck);  
+		  $mymealck->addSoupHandler($mysoupck);
+
+ +And $mymealck->cook([$apiec_of_meat,$avolum_of_soup]) can then done by +just calling: + +
+  $meat_in_mealck->bake($apiece_of_meat);
+  $soup_in_mealck->boil($avolume_of_soup);
+
+  // which effects in $mymealck.m->cook($apiece_o_fmeat);
+  //           and    $mymealck.s ->cook($avolume_of_soup);
+  // that effects again in $mymealck.m->bake($apiece_o_fmeat);
+  //            and    $mymealck.s->boil($avolume_of_soup);
+
+
+ +So this allows for using of manipulation methods of the whole (in case +of simple generic actions reimplemented in the subclasses of the +parts) together with using more specific methods for specific +parts. (e.g. $soup_in_mealck->set_boil_time(10) etc.) + + + +\section securhinei WURH pattern usage in Egwical. + +The Egwical class manages an complete iCalendar component with +VEVENTS, VTODOS etc. as parts. The handling of these part(unions)s is +done by specific subclasses of Egwical, like e.g. infolog_bovtodo and +calendar_bovevents. +The system can be used in two different modes of operation: + +- 1) for conversion of egw tasks, events, etc. to their iCal + counterparts. (the cnv_ methods). + +- 2) for filling the (internal) egwical object with al lot of vtodos and + vevents, either coming from import or from conversion of egw + counterparts. When the compound is filled it can (as a whole) be + exported or imported. + + +add 1) So e.g. exporting a group of events refered to by $event_ids as +VEVENTS goes as follows: +
+$cal = New Bocal;                       // build calendar
+$event_ids = $cal->search("filt_def');  // get group of egw event ids to export.
+
+$ei = New Egwical;		                // build Compound iCalendar processor
+$bve = $ei->addRsc($cal);               // add calendar part and get representive
+$vcalstr = $bve->cnv2VEVENTS($event_ids); // export some events
+
+
+ +if we also want to export some VTODOs this goes as follows: + +
+$binf = New Infolog                     // build infolog app. object
+$task_ids = $binf->search("filt_def');  // get group of egw task ids to export.
+
+$bvt = $ei->addRsc($binf);              // add infolog part and get representive
+$vcalstr = $bvt->cnv2VTODOS($task_ids);
+
+
+add 2) the same egw elements from 1) are now first collected in +egwical and then as a whole exported. + +
+$cal = New Bocal;                       // build calendar
+$event_ids = $cal->search("filt_def');  // get group of egw event ids to export.
+
+$ei = New Egwical;		                // build Compound iCalendar processor
+$bve = $ei->addRsc($cal);               // add calendar sys part and get representive
+	 $bve->clear();                     // empty the list of Vevents in bve
+	 $bve->addEventsOntoVEVENTS($event_ids); // add the converted events to vevents in $ei
+
+
+if we also want to add some VTODOs this goes as follows: +
+$binf = New Infolog                     // build infolog app. object
+$task_ids = $binf->search("filt_def');  // get group of egw task ids to export.
+
+ $bvt = $ei->addRsc($binf);             // add infolog sys part and get representive
+	  $bvt->clear();                    // empty the list of Vtodos  in bvt
+	  $bvt>addTasksOntoVTODOS($task_ids); // add the converted tasks to vtodos in $ei
+
+
+and finally export the whole iCalendar +
+$ei->export()
+
+ + ---------- + +*/ \ No newline at end of file diff --git a/egwical/inc/class.bocalupdate_vevents.inc.php b/egwical/inc/class.bocalupdate_vevents.inc.php new file mode 100644 index 0000000000..f263e65ee5 --- /dev/null +++ b/egwical/inc/class.bocalupdate_vevents.inc.php @@ -0,0 +1,1075 @@ + (This version. new api rewrite, + * refactoring, and extension). + * @author Lars Kneschke (parts from boical that are reused here) + * @author Ralf Becker (parts from boical that are + * reused here) + * @version 0.9.03 (First WURH version, most stuff used from old bovevents class) + * @since 0.9.03 changed mke_RECUR2rar() api + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + */ + class bocalupdate_vevents extends egwical + { + + /** + * @private + * @var boolean + * Switch to print extra debugging about imported and exported events to the httpd errorlog + * stream. + */ + var $evdebug = true; + + /** + * @private + * @var object + * The egw bocal calendar that will be used to transport events from and to + * This is set by setRsc() + */ + var $mycal = null; + + /** + * Describe the provided work capabilities of the class. + * @return string The description as entries for the @ref $reg_workers registry + * table. + */ + function provides_work() + { + return + array('bocalupdate' => array('workerclass' => 'bocalupdate_vevents', + 'workerobj' => null, + 'icalsup' => array('VEVENT')), + // next one is a subclass of bocalupdate, we can work for that too + 'boical' => array('workerclass' => 'bocalupdate_vevents', + 'workerobj' => null, + 'icalsup' => array('VEVENT')), + ); + } + + /** + * Our Constructor, sets the basic class members @ref $ei , @ref supportedFields + * @ref $ical2egwFields and @ref $iprovide_work + */ + function bocalupdate_vevents($prodid='all') + { + // call our abstract superclass constructor + egwical::egwical(); + //@todo rewrite supportedFields setting to distribute it over the egwical + // baseclass and the subclasses cleverly + $this->_set_ical2egwFields(); // add VEVENT and event pairs only + $this->setSupportedFields($prodid); + + return true; + } + + /** + * Set the egw resource that this worker will handle. + * This worker is only capable of handling bocalupdate calendar objects, so it should + * be of that class. This method is mostly called indirectly from a egwical compound + * addRsc() call. But you can call it also directly (if you know what your doing ..) + * @return boolean false on error, true else + */ + function setRsc($egw_rsc) + { + if(!is_a($egw_rsc,'bocalupdate')) + return false; + $this->mycal = $egw_rsc; + return true; + } + + // -------- below only conversion and import/export stuff ----- + + /** + * @private + * @var array $ical2egwFields + * An array containing roughly the mapping from iCalendar + * to egw fields. Set by constructor. + */ + var $ical2egwFields; + + /** + * @private + * @var array $supportedFields + * An array with the current supported fields of the + * importing/exporting device. + * To detect if a certain ical property (eg ORGANIZER) is supported in the current + * data import/export do a isset($this->supportedFields['ORGANIZER']). + * To detect if a certain egw field (eg status) is supported in the current + * data import/export do a + * in_array(array_flatten(array_values($this->supportedFields)),'status') + * or something like that (not tested, implemented, or needed yet..) Maybe should + * implement a method for this.. + * @note This table should probably better be in class @ref egwical + */ + var $supportedFields; + + + + + /** + * @var boolean + * Switch that determines if uid matching is tried. + * + * For a more on uidmatching @see \secimpumatch + * + * If $uid_matching is true then: + * - on import of a vevent the update routines will first try to + * find an existing egw event with the same uid value as present + * in the UID field of the newly to be imported vevent. If this + * succeeds this egw event will get updated with the info from + * the vevent. If this fails a new event will be generated and + * the uid taken from the vevent will be stored in its uid + * field. + * + * if $uid_matching is false then: + * - On import the VEVENT UID field will be checked, if it + * appears to be a previously exported uid value then the + * encoded egw id of the old egw event is retrieved and used for + * update. If it doesnot have a uid value with a valid egw id + * encoding, then the its is handled as being a new VEVENT to be + * imported, and a new egw id will be generated. The old vevent + * uid will though be saved for possible later use, (just as + * with uid_matching on). + */ + var $uid_matching = false; + + /** + * @var boolean + * Switch that determines if events not anymore in egw are allowed to be reimported + * + * Default this is on + */ + var $reimport_missing_events = true; + + + + /** + * Export Egw events and add them to a Horde_iCalendar. + * + * The eGW events in $events are exported to iCalendar VEVENTS and then these are added to + * the Horde_iCalendar object &$hIcal. + * Note that only supported Fields are exported as VEVENTS to the iCalendar. + * + * @section secexpeuid Egw uid export switch + * If $euid_export is set, then for each exported event, the current value of the event uid + * as stored in Egw, will be used to produce a value for the vevent its UID field. When off + * a new UID value will generated with the egw event id encoded. + * + * @param Horde_iCalendar &$hIcal + * object to wich the produced VEvents are added. + * @param array $events the array with eGW events (or event id's) that will be exported + * @param boolean $euid_export switch to enable export of the egw uid fields, when off + * default) the vevents uid fields get a value generated with the egw id encoded. + * @return boolean|int $ok/$vcnt on error: false / on success: nof vevents exported + * @ref $supportedFields determines which fields of VEVENT will be exported + */ + function exportEventsOntoIcal(&$hIcal, $events, $euid_export=false, + $reimport_missing_events=false) + { + $vexpcnt =0; // number of vevents exported + + $veExportFields =& $this->supportedFields; + + if (!is_array($events)) $events = array($events); + + foreach($events as $event) { + // event was passed as an event id + if (!is_array($event)){ + $eid = $event; + if( !$event = $this->mycal->read($eid,null,false,'server')){ + // server = timestamp in server-time(!) + return false; // no permission to read $cal_id + } + // event was passed as an array of fields + } else { + $eid = $event['id']; + // now read it again to get all fields (including our alarms) + $event = $this->mycal->read($eid); + } + + // error_log('>>>>>>>>>>>' .'event to export=' . print_r($event,true)); + + // now create a UID value + if ($euid_export) { + // put egw uid into VEVENT, to allow client to sync with his uids + $eventGUID = $event['uid']; + } else { + $eventGUID = $this->mki_v_guid($eid,'calendar'); + } + + $vevent = Horde_iCalendar::newComponent('VEVENT',$hIcal); + $parameters = $attributes = array(); + // to important to let supportedFields decide on this + $attributes['UID'] = $eventGUID; + + foreach($veExportFields as $veFieldName) { + + switch($veFieldName) { + case 'UID': + // already set + break; + + case 'ATTENDEE': + foreach((array)$event['participants'] as $pid => $partstat) { + if (!is_numeric($pid)) continue; + + list($propval,$propparams) = + $this->mki_vp_4ATTENDEE($pid,$partstat,$event['owner']); + // NOTE: we need to add it already: multiple ATTENDEE fields may be occur + $this->addAttributeOntoVevent($vevent,'ATTENDEE',$propval,$propparams); + } + break; + + case 'CLASS': + $attributes['CLASS'] = $event['public'] ? 'PUBLIC' : 'PRIVATE'; + break; + + // according to rfc, ORGANIZER not used for events in the own calendar + case 'ORGANIZER': + if (!isset($event['participants'][$event['owner']]) + || count($event['participants']) > 1) { + $attributes['ORGANIZER'] = $this->mki_v_CAL_ADDRESS($event['owner']); + $parameters['ORGANIZER'] = $this->mki_p_CN($event['owner']); + } + break; + + // Note; wholeday detection may change the DTEND value later! + case 'DTEND': + // if(date('H:i:s',$event['end']) == '23:59:59') + // $event['end']++; + $attributes[$veFieldName] = $event['end']; + break; + + case 'RRULE': + if ($event['recur_type'] == MCAL_RECUR_NONE) + break; // no recuring event + $attributes['RRULE'] = $this->mki_v_RECUR($event['recur_type'], + $event['recur_data'], + $event['recur_interval'], + $event['start'], + $event['recur_enddate']); + break; + + case 'EXDATE': + if ($event['recur_exception']) { + list( $attributes['EXDATE'], $parameters['EXDATE'])= + $this->mki_vp_4EXDATE($event['recur_exception'],false); + } + break; + + case 'PRIORITY': + if (is_numeric($eprio = $event['priority']) && ($eprio >0) ) + $attributes['PRIORITY'] = $this->mki_v_prio($eprio); + break; + + case 'TRANSP': + $attributes['TRANSP'] = $event['non_blocking'] ? 'TRANSPARENT' : 'OPAQUE'; + break; + + case 'CATEGORIES': + if ($catids = $event['category']){ + $catnamescstr = $this->cats_ids2idnamescstr(explode(',',$catids)); + $attributes['CATEGORIES'] = $catnamescstr; + } + break; + + // @todo find out about AALARM, DALARM, Is this in the RFC !? + case 'AALARM': + foreach($event['alarm'] as $alarmID => $alarmData) { + $attributes['AALARM'] = $hIcal->_exportDateTime($alarmData['time']); + // lets take only the first alarm + break; + } + break; + + case 'DALARM': + foreach($event['alarm'] as $alarmID => $alarmData) { + $attributes['DALARM'] = $hIcal->_exportDateTime($alarmData['time']); + // lets take only the first alarm + break; + } + break; + + case 'VALARM': + foreach($event['alarm'] as $alarmID => $alarmData) { + $this->mki_c_VALARM($alarmData, $vevent, + $event['start'], $veExportFields); + } + break; + + case 'STATUS': // note: custom field in event + if (! $evstat = strtoupper($event['status'])) + $evstat = 'CONFIRMED'; //default.. + $attributes['STATUS'] = $evstat; + break; + + default: + // only use default for level1 VEVENT fields + if(strpos($veFieldName, '/') !== false) + break; + // use first related field only for the simple conversion + $efield = $this->ical2egwFields[$veFieldName][0]; + if ($event[$efield]) { // dont write empty fields + $attributes[$veFieldName] = $event[$efield]; + } + break; + } + + } //end foreach + + // wholeday detector (DTEND =23:59:59 && DTSTART = 00:00) + // if detected the times will be exported in VALUE=DATE format + if(((date('H:i:s',$event['end']) == '23:59:59') || + (date('H:i:s',$event['end']) == '00:00:00')) + && (date('H:i',$event['start'] == '00:00'))){ + $attributes['DTSTART'] = + $this->hi->_parseDate(date('Ymd',$event['start'])); + $attributes['DTEND'] = + $this->hi->_parseDate(date('Ymd',$event['end']+1)); + $parameters['DTEND']['VALUE'] = 'DATE'; + $parameters['DTSTART']['VALUE'] = 'DATE'; + // error_log('WHOLE DAY DETECTED'); + } + + // handle created and modified field setting + $created = $this->get_TSdbAdd($event['id'],'calendar'); + if (!$created && !$modified) + $created = $event['modified']; + if ($created) + $attributes['CREATED'] = $created; + if (!$modified) + $modified = $event['modified']; + if ($modified) + $attributes['LAST-MODIFIED'] = $modified; + + // add all collected attributes (not yet added) to the vevent + foreach($attributes as $aname => $avalue) { + $this->addAttributeOntoVevent($vevent, + $aname, + $avalue, + $parameters[$aname]); + } + $hIcal->addComponent($vevent); + $vexpcnt += 1; + } + + return $vexpcnt; //return nof vevents exported + } + + + + + /** + * Import all VEVENTS from a Horde_iCalendar into Egw + * + * The ical VEVENTS components that are contained in de $hIcal Horde_iCalendar + * are converted to eGW events and imported into the eGW calendar. + * Depending on the value of $importMode, the conversion will generate either eGW + * events with completely new id s (DUPLICATE mode) or generate ids created after + * the VEVENT;UID field so that VEVENTS that refer to already existing eGW events + * will be used to update these (OVERWRITE mode). + * + * @section secimpumatch Uidmatching + * When $uid_matching is not set, the default situation, the uid field of each vevent + * to be imported will examined to check if it has a valid egw id encoded. If so the import + * will try to update the egw event indicated by this id with the contents of the vevent. + * When this doesnot succeed an appropiate error or skip (if you had not enough write rights) + * will be the result. If there can be no valid egw id be decoded, the vevent will be considered + * as a new one and an hence a new egw id will automatically be produced. + * + * When $uid_matching is enabled, the value of the uid field of the vevent will matched against + * all the uid fields of existing egw events. If a matching egw event with id is found, + * the import + * routine will try to update this event. If no success an appropiate error will be generated. + * If no match is found, the import proceeds, just as without uidmatching, by generating a + * new egw event with a new id. The events uid field will be filled with the vevents uid, + * for possible later re-use. + * + * @note Mostly it is best to disable uidmatching. It prevents that multiple duplicates + * of a event will be created in Egw, that may not be accessible anymore via the Ical-Service + * interface. Only use it when you really need to reimport an already once imported calendar + * because you accidentally deleted parts of it in Egw. Better still would be copy these lost + * events into a downloaded version of your original calendar and then update this one without + * the uid_matching enabled. (It has namely no effect for new events and the old (i.e. + * already downloaded to the client) events will be recognized without uidmatching. + * + * @param Horde_iCalendar &$hIcal object with ical VEVENT objects + * @param string $importMode toggle for duplicate (ICAL_IMODE_DUPLICATE) + * or overwrite (ICAL_IMODE_OVERWRITE) import mode + * @param int $cal_id strange parameter, at least for -1 create new events + * and if 0 then always add user to participants + * JVL: THIS NEEDS TO BE CLARIFIED! + * @param boolean $reimport_missing_events enable the import of previously exported events + * that are now gone in egw (probably deleted by someone else) Default false. + * @return boolean| int $false|$evcnt on error: false | on success: nof imported elms + * @ref $supportedFields determins the VEVENTS that will be used for import + */ + function importVEventsFromIcal(&$hIcal, $importMode='OVERWRITE', $cal_id=0, + $reimport_missing_events=false) + { + $overwritemode = stristr($importMode,'overwrite') ? true : false; + $evokcnt = 0; // nof events imported ok + $everrcnt = 0; // nof events imported erroneous + $evskipcnt = 0; // nof events imported skipped (user !== owner) + $evdelcnt = 0; // nof events deleted ok + $evmisskipcnt =0; // nof missing event updates skipped + + $veImportFields =& $this->supportedFields; + +// error_log('veImportFields::'. print_r($veImportFields,true)); + + $eidOk = false; // returning false, if file contains no components + $user_id = $GLOBALS['egw_info']['user']['account_id']; + + foreach($hIcal->getComponents() as $vevent) { + // HANDLE ONLY VEVENTS HERE + if(!is_a($vevent, 'Horde_iCalendar_vevent')) + continue; + +// $event = array('participants' => array()); + $event = array('title' => 'Untitled'); + $alarms = array(); + unset($owner_id); + $evduration = false; + $nonegw_participants = array(); + + // handle UID field always first according to uid_matching algorithm + $cur_eid = false; // current egw event id + $cur_owner_id = false; // current egw event owner id + $cur_event = false; // and the whole array of possibly correspond egw event + // import action description (just for fun and debug) : + // NEW|NEW-NONUID|NEW-FOR-MISSING + // DEL-MISSING|DEL-READ|DEL-READ-UID| + // UPD-MISSING|UPD-READ|UPD-READ-UID + $imp_action = 'NEW-NONUID'; + + if($uidval = $vevent->getAttribute('UID')){ + // ad hoc hack: egw hates slashes in a uid so we replace these anyhow with - + $vuid = strtr($uidval,'/','-'); + $event['uid'] = $vuid; + + if(!$this->uid_matching){ + + // UID_MATCHING DISABLED, try to decode cur_eid from uid + if ($cur_eid = $this->mke_guid2id($vuid,'calendar')){ + // yes a request to import a previously exported event! + if ($cur_event = $this->mycal->read($cur_eid)){ + // oke we can read the old event + $cur_owner_id = $cur_event['owner']; + $imp_action = 'UPD-READ'; + $event['id'] = $cur_eid; + } elseif($reimport_missing_events){ + // else: a pity couldnot read the corresponding cur_event, + // maybe it was deleted in egw already.. + $imp_action = 'UPD-MISSING'; + unset($event['id']); // import as a new one + } else{ + // go on with next vevent + $evmisskipcnt += 1; + continue; + } + }else{ + // no decodable egw id there, so per definition no corresponding egw event + // so will just import the vevent as a new event + $imp_action = 'NEW'; + } + + //UID_MATCHING ENABLED + } elseif($overwritemode && $cal_id <= 0 && !empty($vuid)){ + // go do uidmatching, search for a egw event with the vuid as uid field + if ($cur_event = $this->mycal->read($vuid)) { + $cur_eid = $uidmatch_event['id']; + $cur_owner_id = $uidmatch_event['owner']; + $imp_action = 'UPD-READ-UID'; + $event['id'] = $cur_eid; + }else{ + // uidmatch failed, insert as new + $imp_action = 'NEW'; + } + } + + } + + // lets see what other supported veImportFields we can get from the vevent + foreach($vevent->_attributes as $attr) { + $attrval = $GLOBALS['egw']->translation->convert($attr['value'],'UTF-8'); + + + // SKIP UNSUPPORTED VEVENT FIELDS + if(!in_array($attr['name'],$veImportFields)) + continue; + +// error_log('cnv field:' . $attr['name'] . ' val:' . $attrval); + + switch($attr['name']) { + // oke again these strange ALARM properties... + case 'AALARM': + case 'DALARM': + if (preg_match('/.*Z$/',$attrval,$matches)) { + $alarmTime = $hIcal->_parseDateTime($attrval); + $alarms[$alarmTime] = array('time' => $alarmTime); + } + break; + + case 'CLASS': + $event['public'] = (int)(strtolower($attrval) == 'public'); + break; + + case 'DESCRIPTION': + $event['description'] = $attrval; + break; + + case 'DTEND': + // will be reviewed after all fields are collected + $event['end'] = $attrval; + break; + + // note: DURATION and DTEND are mutually exclusive + case 'DURATION': + // duration after eventstart in secs + $evduration = $attrval; + break; + + case 'DTSTART': + // will be reviewed after all fields are collected + $event['start'] = $attrval; + break; + + case 'LOCATION': + $event['location'] = $attrval; + break; + + case 'RRULE': + // we may need to find a startdate first so delegate to later + // by putting it in event['RECUR'] + $event['RECUR'] = $attrval; + break; + case 'EXDATE': + if (($exdays = $this->mke_EXDATEpv2udays($attr['params'], $attrval)) + !== false ){ + foreach ($exdays as $day){ + $event['recur_exception'][] = $day; + } + } + break; + + case 'SUMMARY': + $event['title'] = $attrval; + break; + + case 'TRANSP': + $event['non_blocking'] = $attrval == 'TRANSPARENT'; + break; + // JVL: rewrite! + case 'PRIORITY': + $event['priority'] = $this->mke_prio($attrval); + break; + + case 'CATEGORIES': + $catnames = explode(',',$attrval); + $catidcstr = $this->cats_names2idscstr($catnames,$user_id,'calendar'); + $event['category'] .= (!empty($event['category'])) + ? ',' . $catidcstr : $catidcstr; + break; + + // when we encounter an new valid cal_address but not yet in egw db + // should we import it? + case 'ATTENDEE': + if ($pid = $this->mke_CAL_ADDRESS2pid($attrval)){ + if( $epartstat = $this->mke_params2partstat($attr['params'])){ + $event['participants'][$pid] = $epartstat; + } elseif ($pid == $event['owner']){ + $event['participants'][$pid] = 'A'; + } else { + $event['participants'][$pid] = 'U'; + } + // egw unknown participant, add to nonegw_participants list + } else { + $nonegw_participants[] = + $this->mke_ATTENDEE2cneml($attrval,$attr['params']); + } + break; + + // make organizer into a accepting participant + case 'ORGANIZER': // make him + if ($pid = $this->mke_CAL_ADDRESS2pid($attrval)) + $event['participants'][$pid] = 'A'; + //$event['owner'] = $pid; + break; + + case 'CREATED': // will be written direct to the event + if ($event['modified']) break; + // fall through + + case 'LAST-MODIFIED': // will be written direct to the event + $event['modified'] = $attrval; + break; + + case 'STATUS': // note: custom field in event + $event['status'] = strtoupper($attrval); + break; + + default: + error_log('VEVENT field:' .$attr['name'] .':' + . $attrval . 'HAS NO CONVERSION YET'); + } + } // end of fields loop + + // now all fields are gathered do some checking and combinations + + // we may have a RECUR value set? Then convert to egw recur def + if ($recurval = $event['RECUR']){ +//error_log('recurval=' . $recurval . '='); + if(!($recur = $this->mke_RECUR2rar($recurval,$event['start'])) == false){ + foreach($recur as $rf => $rfval){ + $event[$rf] = $rfval; + } + } + unset($event['RECUR']); + } + + // build endtime from duration if dtend was not set + if (!isset($event['end']) && ($evduration !== false)){ + $event['end'] = $this->mke_DDT2utime($event['start']) + $evduration; + } + + // a trick for whole day handling or ...?? + if(date('H:i:s',$event['end']) == '00:00:00') + $event['end']--; + + // check vevent for subcomponents (VALARM only at the moment) + // maybe some day do it recursively... (would be better..) + foreach($vevent->getComponents() as $valarm) { + // SKIP anything but a VALARM + if(!is_a($valarm, 'Horde_iCalendar_valarm')) + continue; + $this->upde_c_VALARM2alarms($alarms,$valarm,$user_id,$veImportFields); + } + + // AD HOC solution: add nonegw participants to the description + // should be controlable by class member switch + if (count($nonegw_participants) > 0) + $this->upde_nonegwParticipants2description($event['description'], + $nonegw_participants); + + // handle fixed id call (for boical compatibility) + // @todo test boical compatibility (esp. with $cal_id>0 case) + if($cal_id > 0) { + $event['id'] = $cal_id; + } + + // SORRY THE PARTICPANTS HANDLING OF EGW IS NOT YET CLEAR TO ME (JVL) + // so I do the bold solution to add ourself to participants list if we are not on yet + if(!isset($event['participants'][$user_id])) + $event['participants'][$user_id] = 'A'; + + // error_log('<< ok <<<<' . 'event read for import=' . print_r($event,true)); + + + // -- finally we come to the import into egw --- + + if (($event['title'] == 'X-DELETE') || ($event['title'] == '_DELETED_')){ + + + // -------- DELETION -------------------- + // error_log('delete event=' . print_r($event,true)); + $imp_action = 'DEL-' . $imp_action; + if(! $cur_eid) { + $this->_errorlog_evupd('ERROR: ' . $imp_action, + $user_id, $event, false); + $everrcnt += 1; + continue; + } else { + // event to delete is found readable + if($eidOk = $this->mycal->delete($cur_eid)){ + // DELETE OK + $evdelcnt += 1; + + // ASSUME Alarms are deleted by egw on delete of the event... + // otherwise we should use this code: + // delete the old alarms + //foreach($cur_event['alarm'] as $alarmID => $alarmData) { + // $this->delete_alarm($alarmID); + //} + continue; + } elseif ($user_id != $cur_owner_id){ + // DELETE BAD but it wasnt ours anyway so skip it + if ($this->evdebug) + $this->_errorlog_evupd('SKIPPED: ' . $imp_action . ' (INSUFFICIENT RIGHTS)', + $user_id, $event, $cur_event); + $evskipcnt += 1; + continue; + } else { + // DELETE BAD and it was ours + $this->_errorlog_evupd('ERROR: ' . $imp_action . '(** INTERNAL ERROR ? **)', + $user_id, $event, $cur_event); + $everrcnt += 1; + continue; + } + + } + + // -------- UPDATE -------------------- + } elseif ($eidOk = $this->mycal->update($event, TRUE)){ + // UPDATE OKE ,now update alarms + $evokcnt += 1; // nof imported ok vevents + // handle the found alarms + if(in_array('VALARM',$veImportFields)){ + // delete the old alarms for the event, note: we could also have used $cur_event + // but jus to be sure + if(!$updatedEvent = $this->mycal->read($eidOk)){ + error_log('ERROR reading event for Alarm update, will skip update..'); + continue; + } + + // ******** for serious debugging only.. ************** + // if ($this->evdebug){ + // $this->_errorlog_evupd('OK: ' . $imp_action, + // $user_id, $event, $cur_event); + //error_log('event readback dump:' . print_r($updatedEvent,true)); + // } + // ******** eof serious debugging only.. ************** + + foreach($updatedEvent['alarm'] as $alarmID => $alarmData) { + $this->delete_alarm($alarmID); + } + // set new alarms + foreach($alarms as $alarm) { + if(!isset($alarm['offset'])){ + $alarm['offset'] = $event['start'] - $alarm['time']; + } elseif (!isset($alarm['time'])){ + $alarm['time'] = $event['start'] - $alarm['offset']; + } + $alarm['owner'] = $user_id; +// error_log('setting egw alarm as:' . print_r($alarm,true)); + $this->save_alarm($eidOk, $alarm); + } + } + continue; + + // ---UPDATE BAD -------- + } elseif ($user_id != $cur_owner_id){ + // UPDATE BAD, but other ones event, so skip + if ($this->evdebug) + $this->_errorlog_evupd('SKIPPED: ' . $imp_action . ' (INSUFFICIENT RIGHTS)', + $user_id, $event, $cur_event); + $evskipcnt += 1; + continue; + } else { + // UPDATE BAD and we own it or it was a new one + $this->_errorlog_evupd('ERROR: ' . $imp_action . '(** INTERNAL ERROR ? **)', + $user_id, $event, $cur_event); + $everrcnt += 1; + continue; + } + error_log('CODING ERROR: SHOULDNOT GET HERE'); + } // for each + + if (($everrcnt > 0) || $this->evdebug) + error_log('** user[' . $user_id . '] vevents imports: ' . $everrcnt . ' BAD,' . + $evskipcnt . ' skip-(insufficient rights), ' . $evmisskipcnt . + ' skip-(ignore reimport missings), ' . + $evokcnt . ' upd-ok, ' . $evdelcnt . ' del-ok'); + return ($everrcnt > 0) ? false : $evokcnt+ $evdelcnt; + } + + + /** + * @private + * Log event update problems to http errorlog + * @param string $fault description of the fault type + * @param ind $user_id the id of the logged in user + * @param array $new_event the info converted from the vevent to be imported + * @param array|false $cur_event_ids settings of owner, id and uid field of a possibly found + * corresponding egw event. When no such event found: false. + */ + function _errorlog_evupd($fault='ERROR', $user_id, &$new_event, $cur_event) + { + // ex output: + // ** bovevents import for user(12 [pietje]): ERROR + // current egw event: id=24, owner=34, uid='adaafa'\n + // vevent info event: id=24, owner=--, uid='dfafasdf'\n + + $uname =(is_numeric($user_id)) + ? $user_id . '[' . $GLOBALS['egw']->accounts->id2name($user_id) . ']' + : '--'; + if ($cur_event === false){ + $cid = $cown = $cuid = '--'; + }else{ + $cid = $cur_event['id']; + $cown = $cur_event['owner']; + $cuid = $cur_event['uid']; + } + $nid = ($vi = $new_event['id']) ? $vi : '--'; + $nown = ($vi = $new_event['owner']) ? $vi : '--'; + $nuid = ($vi = $new_event['uid']) ? $vi : '--'; + + error_log('** bovevents import for user (' . $cur_eid . + '['. $uname . ']):' . $fault . '\n' . + 'current egw event: id=' . $cid . ',owner=' . $cown . ',uid=' . $cuid .'\n' . + 'vevent info event: id=' . $nid . ',owner=' . $nown . ',uid=' . $nuid .'\n' ); +// error_log('vevent info event dump:' . print_r($new_event,true) . '\n <<-----------<<\n'); + } + + + /** + * @private + * + * Fill member var that holds the iCalendar property to Egw fields mapping. + * + * Copy keys from this var to the supportedFields member var to allow import/export + * of the field refered to by the key. + * @todo Maybe someday rethink the ical2egwFields trafo system by rewriting it in paths + * starting from iCalendar/Component=>Field or iCalendar/Comp/SubComp etc. + * @see $ical2egwFields member var that holds the mapping + */ + function _set_ical2egwFields() + { + $this->ical2egwFields = + array( + 'UID' => array('uid'), + 'CLASS' => array('public'), + 'SUMMARY' => array('title'), + 'DESCRIPTION' => array('description'), + 'LOCATION' => array('location'), + 'DTSTART' => array('start'), + 'DTEND' => array('end'), + 'DURATION' => array('end-duration'), + 'ORGANIZER' => array('owner'), + 'ATTENDEE' => array('participants'), + 'RRULE' => array('recur_type','recur_interval','recur_data','recur_enddate'), + 'EXDATE' => array('recur_exception'), + 'PRIORITY' => array('priority'), + 'TRANSP' => array('non_blocking'), + 'CATEGORIES'=> array('category'), + 'URL' => array(''), + 'CONTACT' => array(''), + 'GEO' => array(''), + 'CREATED' => array(''), + 'AALARM' => array('alarms'), // NON RFC2445!! + 'DALARM' => array('alarms'), // NON RFC2445!! + 'VALARM' => array('alarms'), + 'VALARM/TRIGGER' => array('alarms/time') + ); + return true; + } + + + /** + * Set the list of ical fields that are supported during the next imports and exports. + * + * The list of iCal fields that should be converted during the following imports and exports + * of VEVENTS is set. This is done by providing a productmanufacturer name and + * (optionally) a prductname. In a small lookup table the set of currently supported + * fields for this is searched and then set thus in the class member @ref $supportedFields. + * + * @note JVL: I can only see sense in defining supported fields in iCal fields as + * these are the fields (terminology) that the devices have in common. + * in addressbook this approach is also --correctly-- taken. Why not here? + * @param string $_productManufacturer a string indicating the device manufacturer + * @param string $_productName a further specification of the current device that is used + * for import or export. + */ + function setSupportedFields($_productManufacturer='file', $_productName='') + { + $defaultFields = array('CLASS','SUMMARY','DESCRIPTION','LOCATION','DTSTART', + 'DTEND','RRULE','EXDATE','PRIORITY'); + // not: 'TRANSP','ATTENDEE','ORGANIZER','CATEGORIES','URL','CONTACT' + + switch(strtolower($_productManufacturer)) { + case 'nexthaus corporation': + switch(strtolower($_productName)){ + default: + // participants disabled until working correctly + // $this->supportedFields = array_merge($defaultFields,array('ATTENDEE')); + $this->supportedFields = $defaultFields; + break; + } + break; + + // multisync does not provide anymore information then the manufacturer + // we suppose multisync with evolution + case 'the multisync project': + switch(strtolower($_productName)) { + case 'd750i': + default: + $this->supportedFields = $defaultFields; + break; + } + break; + case 'sonyericsson': + switch(strtolower($_productName)){ + default: + $this->supportedFields = $defaultFields; + break; + } + break; + + case 'synthesis ag': + switch(strtolower($_productName)){ + default: + $this->supportedFields = $defaultFields; + break; + } + break; + // used outside of SyncML, eg. by the calendar itself ==> all possible fields + case 'file': + case 'all': + $this->supportedFields = + array_merge($defaultFields, + array('ATTENDEE','ORGANIZER','TRANSP','CATEGORIES', + 'DURATION','VALARM','VALARM/TRIGGER')); +// error_log('OKE setsupportedFields (all)to:'. print_r($this->supportedFields,true)); + break; + + // the fallback for SyncML + default: + error_log("Client not found: $_productManufacturer $_productName"); + $this->supportedFields = $defaultFields; + break; + } + } + + + /** + * + * Exports calendar events as an iCalendar string + * + * @note -- PART OF calendar.boical API COMPATIBILITY INTERFACE ----------- + * @param int/array $events (array of) cal_id or array of the events + * @param string $method='PUBLISH' + * @return string|boolean string with vCal or false on error + * (eg. no permission to read the event) + * + * @see _hiCal class member to hold a temporary Horde_iCalendar object + */ + function &exportVCal($events,$version='1.0',$method='PUBLISH') + { + $hIcal = &new Horde_iCalendar; + $euid_export = false; + + // set some header values of the Horde_iCalendar object + $hIcal->setAttribute('PRODID', '-//eGroupWare//NONSGML eGroupWare Calendar ' + . $GLOBALS['egw_info']['apps']['calendar']['version'].'//' + . strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang'])); + $hIcal->setAttribute('VERSION',$version); + $hIcal->setAttribute('METHOD',$method); + + // convert the eGW events to VEVENTS and add them to hIcal + if(!$this->exportEventsOntoIcal($hIcal, $events,$euid_export)) + return false; + + // conversion oke, now let Horde stringify it and deliver as result + $vcal = $hIcal->exportvCalendar(); + // JVL: destroy the object by hand or does automagic this in php ? + $hIcal = null; + + return $vcal; + + } + + + + /** + * Convert VEVENT components from an iCalendar string into eGW calendar events + * and write these to the eGW calendar as new events or changes of existing events + * + * @note -- PART OF calendar.boical API COMPATIBILITY INTERFACE ----------- + * @param string $_vcalData ical data string to be imported + * @param int $cal_id id of the eGW event to fill with the VEvent data + * when -1 import the VEvent content to new EGW events + * (JVL HACK when 0 allow change but no deletion user is added to participants + * if needed) + * @return boolean $ok false on failure | true on success + */ + function importVCal($_vcalData, $cal_id=-1) + { + + $hIcal = &new Horde_iCalendar; + // our (patched) horde classes, do NOT unfold folded lines, + // which causes a lot trouble in the import, so we do it here + $_vcalData = preg_replace("/[\r\n]+ /",'',$_vcalData); + + // let the Horde_iCalendar object parse the Vcal string into its components + if(!$hIcal->parsevCalendar($_vcalData)){ + return FALSE; + } + $importMode = 'OVERWRITE'; + + // now import the found VEVENTS into eGW calendar + if(!$this->importVEventsFromIcal($hIcal, $importMode, $cal_id)) + { + //error_log('importVCal(): errors in importVEventsFromIcal'); + $hIcal = null; + return false; + } + + $hIcal = null; + return true; + } + + + } + + +?> diff --git a/egwical/inc/class.boical.inc.compat.php b/egwical/inc/class.boical.inc.compat.php new file mode 100644 index 0000000000..71d1681c3c --- /dev/null +++ b/egwical/inc/class.boical.inc.compat.php @@ -0,0 +1,69 @@ +ei =& CreateObject('egwical.egwical'); + $this->wkobj =& $this->ei->addRsc($this); + // alternatively the fast, shortcut, road for knowingly experts only: + //$this->wkobj =& CreateObject('egwical.bocalupdate_vevents'); + //$this->wkobj->setRsc($this); + + if ($this->wkobj == false){ + error_log('boical constructor: couldnot add boical resource to egwical: FATAL'); + return false; + } + } + + + // now implement the compatibility methods, that are all moved to egwical! + + function &exportVCal($events,$version='1.0',$method='PUBLISH') + { + return $this->wkobj->exportVCal($events,$version,$method); + } + + function importVCal($_vcalData, $cal_id=-1) + { + return $this->wkobj->importVCal($_vcalData, $cal_id); + } + + + function setSupportedFields($_productManufacturer='file', $_productName='') + { + return $this->wkobj->setSupportedFields($_productManufacturer, $_productName); + } + + } +?> diff --git a/egwical/inc/class.boinfolog_vtodos.inc.php b/egwical/inc/class.boinfolog_vtodos.inc.php new file mode 100644 index 0000000000..d3ad5a9a98 --- /dev/null +++ b/egwical/inc/class.boinfolog_vtodos.inc.php @@ -0,0 +1,561 @@ + * + * -------------------------------------------- * + * This program is free software; you can redistribute it and/or modify it * + * under the terms of the GNU General Public License as published by the * + * Free Software Foundation; either version 2 of the License. * + **************************************************************************/ + + /* JVL Todo V0.7: + * - add structure and API terminology from class.bovevents.inc.php:DONE + * - rewrite all vevent to vtodo strings: DONE.. + * - Maybe add a supportFields system as done in calendar.bovevents, to allow for + * handling vtodos for various devices + * - if done document the supportFields method and show how it can be used + * - find out how to do deletion based on imported VTODOS ? Can that be done? + * - check the usage and conversions of user time and server times + * - add compatibility API for the class infolog.vcalinfolog: DONE but UNTESTED + * - add ORGANIZER export: DONE (V0.51) removed (dont know map field) + * - add ORGANIZER import: ... maybe map t info_responsible + * - add CATEGORIES export: DONE (V0.52) + * - add CATEGORIE import: DONE (V0.7.01) + * - add "subtask" export: DONE (v0.52) + * - add "subtask" import: PARTLY + * - rewrite PRIORITY export: DONE (V0.52) + * - rewrite PRIORITY import:DONE (V0.7.01) + * - repair datecreated: DONT know map field + * - repair date modified export: PARTLY done + * - repair startdate or enddate without time details: DONE (V0.7.02) + */ + + + // require_once EGW_SERVER_ROOT.'/infolog/inc/class.boinfolog.inc.php'; + require_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/Horde/iCalendar.php'; +// require_once EGW_SERVER_ROOT.'/icalsrv/inc/class.egwical.inc.php'; + + /** + * + * iCal vtodos import and export via Horde iCalendar classes + * @note the routines in this package should be used OO only so that de constructor + * can initialize the data common to the import and export routines + * @note this package provides compatibilty routines for class infolog.vcalinfolog + * this can e.g. be used by making infolog.vcalinfolog a simple extension of + * infolog.bovtodos + * + * @todo move the compatibility functions for vcalinfolog completely to the compat class. + * There is no need to have them here anymore. + * @todo rewrite bovtodos to use a ical2egw and supportedFields system + * @todo IMPORTANT rewrite bovtodos to handle uid_matching analogous to bovevents + * + * @package egwical + * @author Jan van Lieshout This version. + * @author Lars Kneschke (parts of reused code) + * @author Ralf Becker (parts of reused code) + * @version 0.9.02 First for use with new WURH egwical class + * @license http://opensource.org/licenses/gpl-license.php GPL - + * GNU General Public License + */ + + class boinfolog_vtodos extends egwical + { + + /** + * @var object + * The egw infolog object that will be used to transport events from and to + * This is set by setRsc() + */ + var $myinf = null; + + + /** + * Describe the provided work capabilities of the class. + * @return string The description as entries for the @ref $reg_workers registry + * table. + */ + function provides_work() + { + return + array('boinfolog' => array('workerclass' => 'boinfolog_vtodos', + 'workerobj' => $null, + 'icalsup' => array('VTODO')), + 'vcalinfolog' => array('workerclass' => 'boinfolog_vtodos', + 'workerobj' => $null, + 'icalsup' => array('VTODO')) + ); + + } + + + /* + * Our Constructor, fills the basic class members + * and set the description of our worker capabilities. + */ + function bovtodos() { + + // call superclass constructor by hand + boinfolog::boinfolog(); + + $this->TASKMAGIC = $GLOBALS['egw_info']['server']['install_id'] + ? $GLOBALS['egw_info']['server']['install_id'] + : 'local'; + + // $this->setSupportedFields(); //not implemented yet + return true; + } + + + /** + * Set the egw resource that this worker will handle. + * This worker is only capable of handling boinfolog objects, so it should + * be of that class. This method is mostly called indirectly from a egwical compound + * addRsc() call. But you can call it also directly (if you know what your doing ..) + * @return boolean false on error, true else + */ + function setRsc($egw_rsc) + { + if(!is_a($egw_rsc,'boinfolog')) + return false; + $this->myinf = $egw_rsc; + return true; + } + + + // --- conversion and import code -- + + /** + * @private + * @var $TASKMAGIC + * Magic unique number used for de/encoding our uids. + * + * This string that contains global unique magic number that is + * unique for our current database installed etc. It is used to recognize + * earlier exported VTODO or VEVENT UID fields as referring to their eGW counterparts. + */ + var $TASKMAGIC='dummy'; + + + + + // Some helper functions first + + + /** + * generate a unique id, with the todo id encoded into it, which can be + * used for later synchronisation. + * + * @param $todo_id string|int eGW id of the content + * @use $TASKMAGIC string that holds our unique ID + * @return false|string on error: false + * on success the global unique id + */ + function _id2guid($todo_id) + { + if (empty($todo_id)) + return false; + + return 'infolog_task'.'-'.$todo_id.'-'. $this->TASKMAGIC; + } + + + + /** + * get the local content id from a global UID + * + * @param string $globalUid the global UID + * @return false|int on error: false + * on success: local egw todo id + */ + function _guid2id($VTodoUID) + { + // error_log('_guid2id: trying to recover id from' . $VTodoUID); + if (!preg_match('/^infolog_task-(\d+)-' . + $this->TASKMAGIC . '$/',$VTodoUID,$matches)) + return false; + + // error_log("_guid2id: found (" . $matches[1] . ")"); + return $matches[1]; + } + + + + /** + * export the eGW todos in $todos to iCalendar VTODOS and add these to + * the Horde_iCalendar object &$hIcal + * Note: that because eGW does not store uid fields for tasks in its db we + * are in general not able to recoginize VTODOS by their uid-field. + * Because of this it is only possible to have a VTODO overwrite an internal + * eGW todo (task) when this VTODO was in an earlier fase build as export of an + * internal eGW todo. In other words to later on change your imported VTODO, + * you first have export it and in the client make your changes on this exemplar. + * + * @param &$hIcal Horde_iCalendar object to wich the produced VTodos are added + * @param $todos array with either id s (tids) for a eGW todoData structs + * or an array of such todoData structs, that will be exported + * @param boolean $euid_export if true export the uid field (Note: Currently not available!) + * else generate a uid from with the task id encoded (Default setting) + * @return $ok/$vcnt boolean/int on error: false / on success: nof vtodos exported + * @use members supportedFields(), _id2guid() + */ + function exportTodosOntoIcal(&$hIcal, $todos, $euid_export=false) + { + //NOTE: $euid_export has currently no effect +# error_log("ical_export_add_Todos here, for " . count($todos) . "todos"); + + $todo = array(); // container for each todo to be exported + $tid = null; // id of the todo to be exported + $vexpcnt =0; // number of vtodos exported +# $options = array('CHARSET' => 'UTF-8','ENCODING' => 'QUOTED-PRINTABLE'); + + + if (!is_array($todos)) $todos = array($todos); + + foreach($todos as $todo) { + // some hocuspocus to handle the polymorphy of the $todos arg + if (!is_array($todo) + && !($todo = $this->myinf->read($todo))){ + + return false; // no permission to read $tid + } + $tid = $todo['info_id']; + // oke, now sure $todo is a todoData array and $tid its info_id field.. + //_debug_array($todo); + + $todo = $GLOBALS['egw']->translation-> + convert($todo,$GLOBALS['egw']->translation->charset(),'UTF-8'); + +# error_log('todo to export=' . print_r($todo,true)); + + //someday: $this->newComponent() ??? + $vtodo = Horde_iCalendar::newComponent('VTODO',$hIcal); + + $vGUID = $this->_id2guid($tid); + + // if (!$euid_export) + // append Non Recoverable so _guid2id() wont recognize it later + // $vGUID .= 'NR'; + $vtodo->setAttribute('UID',$vGUID); + // for subtasks set the parent + // egw2vtodo: info_id_parent => pid -> RELATED-TO:parent_uid + if ($parid = $todo['info_id_parent']) + $vtodo->setAttribute('RELATED-TO', $this->_id2guid($parid)); + + $vtodo->setAttribute('SUMMARY', $todo['info_subject']); + $vtodo->setParameter('SUMMARY', $options); + $vtodo->setAttribute('DESCRIPTION', $todo['info_des']); + $vtodo->setParameter('DESCRIPTION', $options); + if($todo['info_startdate']) + $vtodo->setAttribute('DTSTART', $todo['info_startdate']); + if($todo['info_enddate']) + $vtodo->setAttribute('DUE', $todo['info_enddate']); + $vtodo->setAttribute('DTSTAMP',time()); + + $lastmodDate = $todo['info_datemodified']; + $vtodo->setAttribute('LAST-MODIFIED', $lastmodDate ); + + if ($createDate = $this->get_TSdbAdd($tid,'infolog')){ + $vtodo->setAttribute( 'CREATED', $createDate); + } else { + $vtodo->setAttribute( 'CREATED', $lastmodDate); + } + + // egw2VTOD: owner -> ORGANIZER field + if ($tfrom_id = $todo['info_owner']){ + $mailtoOrganizer = $GLOBALS['egw']->accounts->id2name($tfrom_id,'account_email'); + $vtodo->setAttribute('ORGANIZER', $this->mki_v_CAL_ADDRESS($tfrom_id)); + $vtodo->setParameter('ORGANIZER', $this->mki_p_CN($tfrom_id)); + } + + $vtodo->setAttribute('CLASS', + ($todo['info_access'] == 'public')?'PUBLIC':'PRIVATE'); + // CATEGORIES, value= all category names from info_cat field comma-separated list + // n.b. dont mind catid ==0 (this is none categorie, I think) + if ($catids = $todo['info_cat']){ + $catnamescstr = $this->cats_ids2idnamescstr(explode(',',$catids)); + $vtodo->setAttribute('CATEGORIES',$catnamescstr); + } + + + // egw2vtodo status trafo: + // done -> COMPLETE:lastmoddate, PERCENT-COMPLETE:100, STATUS:COMPLETED + // ongoing -> STATUS: IN-PROCESS + // offer -> STATUS: NEEDS-ACTION, PERCENT-COMPLETE:0 + switch ($todo['info_status']){ + case 'done': + $vtodo->setAttribute('COMPLETED',$lastmodDate); // for ko35, lastmod? + $vtodo->setAttribute('PERCENT-COMPLETE','100'); + $vtodo->setAttribute('STATUS','COMPLETED'); + break; + case 'ongoing': + $vtodo->setAttribute('STATUS','IN-PROCESS'); + break; + case 'offer': + $vtodo->setAttribute('STATUS','NEEDS-ACTION'); +# $vtodo->setAttribute('PERCENT-COMPLETE',"0"); + break; + default: + // check for percentages + if (ereg('([0-9]+)%',$todo['info_status'],$matches)){ + $vtodo->setAttribute('PERCENT-COMPLETE',$matches[1]); + $vtodo->setAttribute('STATUS','IN-PROCESS'); + }else{ + $vtodo->setAttribute('STATUS','NEEDS-ACTION'); + } + } + + if (is_numeric($eprio = $todo['info_priority']) && ($eprio >0) ) + $vtodo->setAttribute('PRIORITY', + $this->mki_v_prio($eprio) ); + +# $vtodo->setAttribute('TRANSP','OPAQUE'); + + $hIcal->addComponent($vtodo); + $vexpcnt += 1; + } + + return $vexpcnt; //return nof vtodos exported + } + + + + /* @note PART OF COMPATIBILITY API for INFOLOG.VCALINFOLOG + * @note UNTESTED + * Export a single eGW task as a VTODO string + * + * @param $_taskID int/string id of the eGW task to be exported + * @param $_version string version the produced iCalendar content should get + * @return false|string on error | content of the resulting VTODO iCal element + */ + function exportVTODO($_taskID, $_version) + { + $hIcal = &new Horde_iCalendar; + $hIcal->setAttribute('VERSION',$_version); + $hIcal->setAttribute('METHOD','PUBLISH'); + + if(! $tcnt = $this->exportTodosOntoIcal(&$hIcal, array($_taskID), true)) + return false; + + return $hIcal->exportvCalendar(); + } + + + + /** + * Convert the ical VTODOS components that are contained in de $hIcal Horde_iCalendar + * to eGW todos and import these into the eGW calendar. + * Depending on the value of $importMode, the conversion will generate either eGW + * todos with completely new id s (DUPLICATE mode) or try to recover an egw id from + * the VTODO;UID field (so called OVERWRITE mode). Note that because eGW currently + * does not store todo uid field info in its database, such recovering is only + * possible for previously exported todos. + * + * @param &$hIcal Horde_iCalendar object with ical VTODO objects + * @param $importMode string toggle for duplicate (ICAL_IMODE_DUPLICATE) + * or overwrite (ICAL_IMODE_OVERWRITE) import mode + * @return $false|$timpcnt on error: false | on success: nof imported elms + * @use .supportedFields() to steer the VTODOS to eGW todos conversion + * @use members _guid2id() + */ + function importVTodosFromIcal(&$hIcal, $importMode='DUPLICATE') + { + + $overwritemode = stristr($importMode,'overwrite') ? true : false; +# $ftid = $fixed_taskId; + $timpcnt = 0; // nof todos imported + $tidOk = true; // return true, if hIcal contains no vtodo components + + foreach($hIcal->getComponents() as $component) { + // ($ftid < 0) => recover id (overwritemode) or use no id + // ($ftid > 0) => use this value to set the id (compatibility mode) + + if(is_a($component, 'Horde_iCalendar_vtodo')){ + $tidOk = $this->_importVTodoIcalComponent(&$component, $overwritemode, -1); + if (!$tidOk){ + error_log('infolog.bovtodos.importVTodosFromIcal(): ' + . ' ERROR importing VTODO '); + break; // stop at first error + } + + + $timpcnt += 1; // nof imported ok vtodos + } + } + return (!$tidOk) ? false : $timpcnt; + } + + + /* convert a single vtodo horde icalendar component to a eGW todo and write it to + * the infolog system. + * + * @note this routine should better not be exported + * @param &$hIcalComponent Horde_iCalendar_vtodo element that contains the VTODO + * that is to be converted and imported + * @param $overwriteMode boolean generate a new eGW todo (when false) or allow + * overwrite of an existing one (when true) + * @param $newtask_id int/string if >0 : the id of the eGW todo that must be + * overwritten. if <0 : generation of new task or recover taskId from UID field + * @return false | int on error: false | on success: the id of the eGW todo + * that was produced/changed + */ + function _importVTodoIcalComponent(&$hIcalComponent, $overwriteMode, $newtask_id) + { + $ftid = $newtask_id; + $todo = array(); //container for eGW todo + $user_id = $this->owner; // we logged in? + + if(!is_a($hIcalComponent, 'Horde_iCalendar_vtodo')) + return false; + + if($ftid > 0) { + // just go for a change of the content of the eGW task with id=$ftid + $todo['info_id'] = $ftid; + // we will now ignore a UID field later in this VTodo + } + + // now process all the fields found + foreach($hIcalComponent->_attributes as $attributes) { +# error_log( $attributes['name'].' - '.$attributes['value']); + //$attributes['value'] = + // $GLOBALS['egw']->translation->convert($attributes['value'],'UTF-8'); + switch($attributes['name']){ + case 'UID': + if ($ftid > 0) // fixed id mode so we got id from $newtask_id + break; + $vguid = $attributes['value']; + if( $overwriteMode && $tid = $this->_guid2id($vguid)){ + // an old id was recovered from the UID field, we will use it + $todo['info_id'] = $tid; + #error_log('import: using existing id:'.$tid); + } // else we leave the info_id empty so automatically a new todo gets created + break; + // rfc s4.8.1.3.egw2vtodo: public|private|confidential + case 'CLASS': + $todo['info_access'] = strtolower($attributes['value']); + break; +# case 'ORGANIZER': +# $todo['info_from'] = $attributes['value']; +# may be put it in info_responsible field ? +# // full name + mailto see bovevents on a method to handle this + break; + + case 'DESCRIPTION': + $todo['info_des'] = $attributes['value']; + break; + case 'DUE': + $todo['info_enddate'] = $this->mke_DDT2utime($attributes['value']); + break; + case 'DTSTART': + $todo['info_startdate'] = $this->mke_DDT2utime($attributes['value']); + break; + case 'PRIORITY': + $todo['info_priority'] = $this->mke_prio($attributes['value']); + break; + // rfc s4.8.1.11 egw2vtodo status trafo: (now use it backwards) + // done -> COMPLETE:lastmoddate, PERCENT-COMPLETE:100, STATUS:COMPLETED + // ongoing -> STATUS: IN-PROCESS + // offer -> STATUS: NEEDS-ACTION, PERCENT-COMPLETE:0 + case 'STATUS': + switch (strtolower($attributes['value'])){ + case 'completed': + $todo['info_status'] = 'done'; + break; + case 'cancelled': + $todo['info_status'] = 'done'; + break; + case 'in-process': + $todo['info_status'] = 'ongoing'; + break; + default: + // probably == 'needs-action' + $todo['info_status'] = 'offer'; + } + break; + // date and time when completed + case 'COMPLETED': + $todo['info_status'] = 'done'; + break; + case 'PERCENT-COMPLETE': + $pcnt = (int) $attributes['value']; + if ($pcnt < 1) { + $todo['info_status'] = 'offer'; + }elseif($pcnt > 99) { + $todo['info_status'] = 'done'; + }else{ + $todo['info_status'] = $pcnt . '%'; // better should do rounded to 10s + } + break; + case 'SUMMARY': + $todo['info_subject'] = $attributes['value']; + break; + case 'RELATED-TO': + $todo['info_id_parent'] = $this->_guid2id($attributes['value']); + break; + // unfortunately infolog can handle only one cat atm + case 'CATEGORIES': + $catnames = explode(',',$attributes['value']); + $catids = $this->cats_names2idscstr($catnames,$user_id,'infolog'); + $todo['info_cat'] = $catids; + break; + + case 'LAST-MODIFIED': + $todo['info_datemodified'] = $attributes['value']; + break; + + default: +// error_log('VTODO field:' .$attributes['name'] .':' +// . $attributes['value'] . 'HAS NO CONVERSION YET'); + } + } + // error_log('todo=' . print_r($todo,true)); + + if($todo['info_subject'] == 'X-DELETE' && $tid){ + // delete the todo (secret HACK, donot use...) + return $this->myinf->delete($tid); + }else{ + $tidOk = $this->myinf->write($todo,true,false); + // error_log('ok import id:'. $tidOk .' VTODO UID:' . $vguid); + return $tidOk; + } + } + + + + + + /* @note PART OF COMPATIBILITY API for INFOLOG.VCALINFOLOG + * @note UNTESTED + * Import a single iCalendar VTODO string as eGW task(aka: todo) + * + * @param $_vcalData string content of an VTODO iCalender element + * @param $_taskID int/string id of the eGW task to be overwritten + * when < 0 a new task will get produced + * @return false | int on error: false | on success: the id of the eGW todo + * that was produced/changed + */ + function importVTODO(&$_vcalData, $_taskID=-1) + { + $hIcal = &new Horde_iCalendar; + if(!$hIcal->parsevCalendar($_vcalData)) + return FALSE; + + $components = $hIcal->getComponents(); + if(count($components) < 1) + return false; + + return $this->_importVTodoIcalComponent(&$components[0], + true, $_taskID); + } + + + + } + + +?> diff --git a/egwical/inc/class.egwical.inc.php b/egwical/inc/class.egwical.inc.php new file mode 100644 index 0000000000..0bd5ee97a5 --- /dev/null +++ b/egwical/inc/class.egwical.inc.php @@ -0,0 +1,1278 @@ + + *
  • mki_ + * This is for methods that MaKe a Ical thing like a component, field, fieldvalue or + * fieldparameter. Thus these are subdived in:
  • + *
      + *
    • mki_c to make ical Components like VEVENTS or VALARMS
    • + *
    • mki_v to make ical field Values like e.g. a ATTENDEE field value
    • + *
    • mki_vp to make both ical field Values and Parameters
    • + *
    + * + *
  • mke_ + * This is for methods that MaKe a Egw things like a field of egw event or task.
  • + *
  • updi_ + * This is for methods that UPDate an Ical component or field. Note that the Ical component + * to be updated will be passed by reference to these routines
  • + *
  • upde_ + * This is for methods that UPDate an Egw entity like an event or task or.... Note again + * that the Egw entity will be passed by reference to these routines.
  • + * + * + * @section secworkersubclasses Worker Subclasses + * Egwical is implemented following the WURH pattern (see @ref pageegwicalwurh). + * Currently there are two worker subclasses available: + * - bocalupdate_vevents to convert between egw calendar events and VEVENTS + * and allow import and export of these. When added as a resource to an Egwical object, + * the representative member for this subclass is set in the attribute $this->buverpr + * + * - boinfolog_vtodos to convert between egw infolog tasks events and VTODOS + * and allow import and export of these. When added as a resource to an Egwical object, + * the representative member for this subclass is set in the attribute $this->ivtrpr + * + * @note At this moment (V 0.9.01) there is still a little unpleasant piece of extra + * coding needed to get + * it working: when you develop a new workerssubclass you must enter it manually in + * the egwical constructor, + * so that it can later on automatically be found and matched to the appropiat egw resource. + * (I couldnot find an easy way to search for existing subclasses so therefore it is needed) + * + * @note in future this package may become a full wrapper around the Horde_iCalendar + * libraries. This way we can keep the relevant egw and ical objects alive between + * calls so that code could be made faster and less memory consuming.. + * + * @since 0.9.0 api is incompatible with lower versions + * @author Jan van Lieshout (This version) + * @author Lars Kneschke (original code of reused parts) + * @author Ralf Becker (original code of reused parts) + * + * + * @version 0.9.04 (First wurh pattern implementation, with RRULE count= impl.) + * @date 20060216 + * @license http://opensource.org/licenses/gpl-license.php GPL - + * GNU General Public License + */ + class egwical + { + + /** + * @var Horde_iCalendar + * Placeholder object used to access various Horde_iCalendar methods + * In here the constructor will create a Horde_iCalendar object that can be used + * by the various routines in the class + */ + var $hi; + + // ----------- first the implementation of the WURH pattern ------------ + + +// /** Description of the resource handling capacity this object provides +// * @var array +// * Decsription, in the form of an entry for the @ref $reg_rscworkers registry, +// * of the worker capabilities this object provides. +// * When an object of this (sub) class is instatiated and added to a egwical object as +// * a socalled worker representative, this capability description is copied into the +// * (compounds) reg_rscworkers registry. +// * @note in genuine worker subclasses this variable must be initialized +// * by the constructor. +// */ +// var $iprovide_work = null; + + + /** Registry for Resource Worker classes + * + * @var array + * The resource workers registry is an array that holds + * an entry for each type of resource classs with the appropiate worker class and + * the currently set worker (representative) instantiated object of that class. + * @note the (instantiated) worker representative object in the registry entry is + * firstly set when an appropiate worker subclass is actually instantiated during + * runtime. (This setting then is done by the constructor of the worker class of course. + * that uses the info from the workerclass its variable @ref $iprovide_rscwork) + * + * Structure of the registry: + *
    +	   *  $reg_rscworkers = array($rsclass_name =>
    +	   *                          array('workerclass' =>$workerclass_name,
    +	   *                                'workerobj'   => $worker_obj,
    +	   *                                'vcsup'     =>array($velmc1,$velmc2,..)),
    +	   *                           ..)
    +	   *
    + * example of an entry: + *
    +	   *  $reg_rscworkers['boinfolog'] = array('workerclass' =>'boinfolog_vtodos',
    +	   *                                       'workerobj'   => null,
    +	   *                                       'icalsup'     =>array('VTODO'))
    +	   * 
    + */ + var $reg_rscworkers = array(); + + + /** Registry for Resources + * + * @var array + * + * The resources registry is an array that holds an entry for + * types of icalendar component class ('VEVENT, VTODO etc.) + * with: + *- 1) an appropiate egw resource object for storing and + * retrieving the info for these components (thus a calendar- or + * infolog- or .. etc. object) and + *- 2) an (instantiated) worker object to handle transport from + * and to the resource. + * The entries of this registry get filled when the + * resources are added to the egwical object by calls to the + * addRsc() method. + * @note multiple entries for the same icalendar component are allowed. + * This can occur when multiple resources can provide e.g. VEVENT data. + * + * Structure of the registry: + *
    +	   *   reg_rscs = array($icalcompclass_name => array($rsc_obj, $worker_obj),..)
    +	   *
    + * example of an entry: + *
    +	   *  reg_rscs['VTODO'] = array(null, null)
    +	   * 
    + */ + var $reg_rscs = array(); + + + + /** + * Constructor, init the auxiliary object @ref $hi and @ref $TASKMAGIC + * and instantiate the @ref $reg_rscworkers workers registry and the + * @ref $reg_rscs resources + * registry. + * @note At this moment (V 0.9.01): when you develop a new workerssubclass you must + * manually add its characteristics via a class call to its provides_work() function + * from within the constructor code. + * (It is needed because I couldnot find an easy way to search for existing subclasses + * yet) + * + */ + function egwical() + { + // actually this would only be needed by the abstract superclass? + $this->hi = &new Horde_iCalendar; + + $this->TASKMAGIC = $GLOBALS['egw_info']['server']['install_id'] + ? $GLOBALS['egw_info']['server']['install_id'] + : 'local'; + // update following list when a new worker subclasses becomes available + // (i.e. copy the info from its $iprovide_work var) + $this->reg_rscworkers = + array_merge( + $this->reg_rscworkers, + bocalupdate_vevents::provides_work(), + boinfolog_vtodos::provides_work() + ); + // error_log('**now reg_workers=' . print_r($this->reg_rscworkers,true)); + $this->reg_rscs = array(); + } + + + /** + * Add a egw resource. + * + * The egw resource (calendar, infolog, ..) in $egw_rsc is added + * to egwical. This will be used for storing and retrieving + * types of iCalendar components (VEVENT, VTODO,..) for which it has corresponding + * egw data elements (like egw events or egw tasks. + * + * Following the WURH pattern, adding the egw resource $rsc will result in + * the creation of a dedicated representative "worker" subclass object to handle the + * transport to and from the resource. The appropiate worker class is looked up in the + * $reg_workers registry. When the worker is created, the resource and its worker will + * be entered as a resource worker pair in the egwical $reg_rscs registry. + * This representative worker object will also be + * returned by reference to the caller. Using this reference the caller can later on do + * specific settings for the worker, (like setSupportedFields()). + * + * @param object $egw_rsc egw resource (like calendar or infolog,..) object that will be + * used by the egwical representative worker class to transport converted + * ical components to and from. + * @return egwical|false $rpr_worker the representative worker class for handling the + * added egw resource. On error false is returned. + */ + function addRsc($egw_rsc) + { + //step 1: detect the egw_rsc class + if (!$rsc_classname = get_class($egw_rsc)) + // bad value for $egw_rsc + return false; + + //step 1a check that the egwrsc is not already registered + foreach($this->reg_rscs as $vc => $rscwkpair){ + if($rscwkpair[0] === $egw_rcs){ + error_log('Warning: egwical.addRsc(): trying add resource multiple times,' . + ' for class: ' . $rsc_classname . ' : ignored'); + return $rscwkpair[1]; + } + } + + //step 1: detect the egw_rsc class + if (!$rsc_classname = get_class($egw_rsc)) + // bad value for $egw_rsc + return false; + + // step 2a: look in reg_rscworkers for appropiate worker class + if (!$wkprov = $this->reg_rscworkers[$rsc_classname]){ + error_log('Error: egwical.addRsc(): no workerclass available yet for resourcetype:' . + $class_name . ' sorry'); + return false; + } + + // step 2b create a workerobject of the correct type + // note that we dont reuse an already available workerobj of the right type because + // probably the workerclasses are not reentrantly coded yet... + if(! $wkobj =& CreateObject('egwical.' . $wkprov['workerclass'])){ + // workerclass object creation problem + return false; + } + // step 2c give the workerobj the egw_rsc to handle + $wkobj->setRsc($egw_rsc); + + // step3: use the found worker info to register the egw_rsc and workerobj for + // each of the supported type of ical elements + foreach ($wkprov['icalsup'] as $icomptype ){ + //error_log('adding reg_rscs['. $icomptype . '] entry'); + $this->reg_rscs = array_merge($this->reg_rscs, + array($icomptype => array($egw_rsc, $wkobj))); + } + // step 4: return the workerclass (representative obj) + + return $wkobj; + } + + + + // ------------- second: below only generic conversion stuff -------------- + + + // --- generic conversion auxilliary routines ------------- + // --- note: this could be left out in the abstract baseclass instantiation + // but is not that much, so leave it get duplicated... + + /** + * @private + * @var string + * Magic unique number used for de/encoding our uids. + * + * This string that contains global unique magic number that is + * unique for our current database installed etc. It is used to recognize + * earlier exported VTODO or VEVENT UID fields as referring to their eGW counterparts. + */ + var $TASKMAGIC='dummy'; + + + /** + * @var array $status_ical2egw + * Conversion of the egw used priority values(0..3) to corresponding ical values(0..9). + * @private + */ + var $priority_egw2ical = + array( + 0 => 0, // undefined + 1 => 9, // low + 2 => 5, // normal + 3 => 1, // high + ); + /** + * @var array $status_ical2egw conversation of the priority ical => egw + * Conversion of the icalendar used priority values(0..9) to corresponding egw values (0..3). + * @private + */ + var $priority_ical2egw = + array( + 0 => 0, // undefined + 9 => 1, 8 => 1, 7 => 1, // low + 6 => 2, 5 => 2, 4 => 2, // normal + 3 => 3, 2 => 3, 1 => 3, // high + ); + + + /** + * @var array $partstatus_egw2ical + * Conversion of the egw used participant status values to the corresponding icalendar + * attendee status terminology. + * @private + */ + var $partstatus_egw2ical = + array( + 'U' => 'NEEDS-ACTION', + 'A' => 'ACCEPTED', + 'R' => 'DECLINED', + 'T' => 'TENTATIVE', + ); + /** + * @var array + * Conversion of the icalendar used attendee status values to the corresponding icalendar + * participants status terminology. + * @private + */ + var $partstatus_ical2egw = + array( + 'NEEDS-ACTION' => 'U', + 'ACCEPTED' => 'A', + 'DECLINED' => 'R', + 'TENTATIVE' => 'T', + ); + + + /** + * @var array $recur_egw2ical + * Conversion of egw recur-type to ical FREQ values for RRULE fields + * @private + */ + var $recur_egw2ical = + array( + MCAL_RECUR_DAILY => 'DAILY', + MCAL_RECUR_WEEKLY => 'WEEKLY', + MCAL_RECUR_MONTHLY_MDAY => 'MONTHLY', + MCAL_RECUR_MONTHLY_WDAY => 'MONTHLY', + MCAL_RECUR_YEARLY => 'YEARLY', + ); + // BYMONHTDAY={1..31}, BYDAY={1..5}{MO..SO} + + /** + * @var array + * recur_days translates MCAL recur-days to verbose labels + * (copied from class.bocal.inc.php file + * @private + */ + var $recur_days = + array( + MCAL_M_MONDAY => 'Monday', + MCAL_M_TUESDAY => 'Tuesday', + MCAL_M_WEDNESDAY => 'Wednesday', + MCAL_M_THURSDAY => 'Thursday', + MCAL_M_FRIDAY => 'Friday', + MCAL_M_SATURDAY => 'Saturday', + MCAL_M_SUNDAY => 'Sunday', + ); + + /** + * @var array + * Get sequential indexes for the daynames in a week. Used for recurrence count + * calculations. + */ + var $dowseqid = + array('SU' => 1, 'MO' => 2, 'TU' => 3, 'WE' => 4, 'TH' => 5, 'FR' => 6, 'SA' => 7); + + + // --- generic conversion auxilliary routines ------------- + + /** + * Parse a vCalendar string into an Horde_iCalendar object. + * + * To actually parse the string, the Horde_iCalendar in member @ref $hi is used. + * @param string $vcalstr the icalendar input string + * @return boolean|Horde_iCalendar the resulting parsed elements collected in a + * horde ical object. On error: false + */ + function parsevCalendar($vcalstr) + { + // unfoldlines as this was removed from our horde stuff + $vcalstr = preg_replace("/[\r\n]+ /",'',$vcalstr); + + // $this->hi->clear(); + if(!$this->hi->parsevCalendar($vcalstr)){ + error_log('egwical parsevCalendar: ERROR- couldnot parse..'); + return false; + } + + return $this->hi; + } + + + + + /** + * Generate ical UID from egw id. + * + * generate a unique id, with the egw id encoded into it, which can be + * used for later synchronisation. + * @param string|int $egw_id eGW id of the egw entity (event, task,..) + * @param string $app_prefix prefix to use in ecnoding the name + * + * @return string|false on success the global unique id. On error: false. + * + * Uses @ref $TASKMAGIC string that holds our unique ID + */ + function mki_v_guid($egw_id,$app_prefix='egw') + { + if (empty($egw_id)) + return false; + return $app_prefix .'-' . $egw_id. '-' . $this->TASKMAGIC; + } + + + + /** + * Try to decode an egw id from a ical UID + * + * @param string $guid the global Icalendar UID value + * @param string $app_prefix prefix to be found in the encoding + * @return false|int On error: false. + * On success: local egw todo id. + */ + function mke_guid2id($guid,$app_prefix='egw') + { + // error_log('mke_guid2id: trying to recover id from' . $guid); + if (!preg_match('/^' . $app_prefix . '-(\d+)-' . + $this->TASKMAGIC . '$/',$guid,$matches)) + return false; + + // error_log("mke_guid2id: found (" . $matches[1] . ")"); + return $matches[1]; + } + + + + /** + * Get database add date of event or todo + * @private + * @param int $id id of event or todo + * @param string $appname name of the application (='calendar' or 'infolog') + * @return int $createdate of db insert or false on error + */ + function get_TSdbAdd($id,$appname='calendar') + { + if (!(($appname == 'calendar') || ($appname == 'infolog'))) + return false; + if (! $auid = $GLOBALS['egw']->common->generate_uid($appname,$id)) + return false; + + return $GLOBALS['egw']->contenthistory->getTSforAction($auid,'add'); + } + + + /** + * Convert a egw prio into a value for the ical property PRIORITY + * @param int $eprio priority in egw (0..3) + * @return int $iprio conversion of $eprio as value (0..9) for the ical PRIORITY prop + */ + function mki_v_prio($eprio = 0) + { + return $this->priority_egw2ical[$eprio]; + } + + /** + * Convert a ical prio into a value for egw + * @param int $iprio priority in ical (0..9) + * @return int $eprio conversion of $iprio as value (0..3) for egw + */ + function mke_prio($iprio = 0) + { + return $this->priority_ical2egw[$iprio]; + } + + + + /** + * Translate cat-ids to array with id-name pairs + * + * JVLNOTE: boldly copied from class.xmlrpc_server.inc.php because I donot know how + * to instantiate $GLOBALS['server'] (that provides this method) atm. + * @note THIS CODE SHOULD BE SOMEWHERE ELSE: IT HAS NOTHING TO DO WITH ICAL!! + * @param array $cids the list with category ids + * @return string|false $idnamescstr commasep string with names for the category ids or + * on error false + */ + function cats_ids2idnamescstr($cids) + { + if(empty($cids)) + return false; + + if (!is_object($GLOBALS['egw']->categories)) + $GLOBALS['egw']->categories = CreateObject('phpgwapi.categories'); + + $idnames = array(); + foreach($cids as $cid) { + if ($cid) + $idnames[$cid] = stripslashes($GLOBALS['egw']->categories->id2name($cid)); + } + return implode(',',$idnames); + } + + + // ************ JVL CHECK THE CODE BENEATH ***************** + // oke: seems to work for a single categorie (tested form bovtodos calls) + + /** + * Translate catnames back to cat-ids creating/modifying cats on the fly + * + * JVLNOTE boldly copied from class.xmlrpc_server.inc.php because I donot know how + * to instantiate $GLOBALS['server'] (that provides this method) atm. + * + * @note THIS CODE SHOULD BE SOMEWHERE ELSE: IT HAS NOTHING TO DO WITH ICAL!! + * @param array $cnames list with category names + * @return string $cidscstr commasep string with ids generated or found for + * the category names. + */ + function cats_names2idscstr($cnames,$owner_id,$app_name='infolog') + { + if (empty($cnames)) + return false; + + if (!is_object($catsys =& $GLOBALS['egw']->categories)) { + $GLOBALS['egw']->categories =& CreateObject('phpgwapi.categories', + $owner_id,$app_name); + $catsys =& $GLOBALS['egw']->categories; + } + $cids = array(); + foreach($cnames as $name) { + if (!($cid = $catsys->name2id($name))) { + // existing cat-name use the id + // new cat + $cid = $catsys->add(array('name' => $name,'descr' => $name)); + } + $cids[] = (int)$cid; + } + return implode(',',$cids); + } + + + /** + * Convert and egw account id into a iCalendar CAL-ADDRESS type value string + * @param int $aid egw account(person) id + * @return string $cls cal_address format string (mailto:. On error + * the emailadr part will stay empty. + */ + function mki_v_CAL_ADDRESS($aid) + { + $mailtoAid = $GLOBALS['egw']->accounts->id2name($aid,'account_email'); + return $mailtoAid ? 'MAILTO:'.$mailtoAid : 'MAILTO:'; + } + + + /** + * Convert and egw account id into a iCalendar CN type parameter string + * @param int $account_id egw account(person) id + * @return string $cnparam CN param value format string. On erro this will be empty. + */ + function mki_p_CN($account_id) + { + $cns = trim($GLOBALS['egw']->accounts->id2name($account_id,'account_firstname') + . ' ' + . $GLOBALS['egw']->accounts->id2name($account_id,'account_lastname')); + + return array('CN' => $cns ? $cns : ''); + } + + + /** + * Convert a horde_icalendar parsed attribute date- or date-time value + * to a unix timestamp. + * @note this is just a hack because horde_icalendar converts only date-times to utime + * @param array|string $ddtval DATE array or DATE-TIME utime string + * @return int $utime unix time of the date or date time + */ + function mke_DDT2utime($ddtval) + { + if(!is_array($ddtval)){ + // assume an already parsed(by Horde_iCalendar) date-time value + return $ddtval; + } else { + //assume a DATE, BUT WHERE DO I GET A POSSIBLE TIMEZONE FROM? + // assume user time zone (for utc use gmmktime() + return @mktime(0,0,0,$ddtval['month'],$ddtval['mday'],$ddtval['year']); + } + } + + + /** + * Convert a unix timestamp to a 6 field hash array in the current active timezone + * + * This is basically alike the php getdate() function but with different field names + * + * The a6date array has fields as in the php getdate() function: + * - year four digit year field + * - month integer month number + * - mday integer day of month number + * - hour integer hour + * - minute integer minutes + * - second integer seconds + * + * @param int $utime a unixtimestamp assumed in utc timezone + * @return array The date in a6date in local timezone format. + */ + function utimetoa6($utime) + { + $t=getdate($utime); + return array('hour' => $t['hours'], 'minute' => $t['minutes'], + 'second' => $t['seconds'],'month' => $t['mon'], + 'mday' => $t['mday'],'year' => $t['year']); + } + + + /** + * Convert a 6 field hash array in the current active timezone to a unix timestamp. + * + * This is basically the inverseof php getdate() function. + * + * The a6date array has fields as in the php getdate() function: + * - year four digit year field + * - month integer month number note: mon, not month!! + * - mday integer day of month number + * - hour integer hour + * - minute integer minutes + * - second integer seconds + * + * @param array $a6 The date in a6date in local timezone format. + * @return int a unixtimestamp assumed in utc timezone + */ + function a6toutime ($a6) + { + return mktime($a6['hour'],$a6['minute'],$a6['second'], + $a6['month'],$a6['mday'],$a6['year']); + } + + + /** + * Convert the egw person id and its participant status into + * an ATTENDEE value and parameterslist + * + * The resulting value of the ATTENDEE field will be in CAL_ADDRESS type format. + * The resulting parameterlist may contain fields of the following: + * - ROLE={CHAIR|REQ-PARTICIPANT|OPT-PARTICIPANT|NON-PARTICIPANT} + * this parameter is NOT used by eGW atm. + * - RSVP={TRUE|FALSE} + * resonse is expected, not set in eGW then status will have value U. + * - PARTSTAT={NEEDS-ACTION|ACCEPTED|DECLINED|TENTATIVE|DELEGATED| + * COMPLETED|IN-PROGRESS} everything from delegated is NOT used by eGW atm. + * - CUTYPE={INDIVIDUAL|GROUP|RESOURCE|ROOM|UNKNOWN} only GROUP or INDIVIDUAL + * are produced atm. + * + * @param int $pid egw id of a participant + * @param array $partstat egw particpant status of person with $uid + * @param int $owner_id id of the owner of the todo or event (needed to set the CHAIR) + * @return array ($val,$params) list with value and parameter-array for ATTENDEE property + * @note no error handling atm + */ + function mki_vp_4ATTENDEE($pid,$partstat,$owner_id) + { + $atdval = $this->mki_v_CAL_ADDRESS($pid); + // first parameter + $atdpars = $this->mki_p_CN($pid); + $atdpars['ROLE'] = ($pid == $owner_id) ? 'CHAIR' : 'REQ-PARTICIPANT'; + $atdpars['RSVP'] = $partstat == 'U' ? 'TRUE' : 'FALSE'; + $atdpars['CUTYPE'] = $GLOBALS['egw']->accounts->get_type($uid) == 'g' + ? 'GROUP' : 'INDIVIDUAL'; + $atdpars['PARTSTAT'] = $this->partstatus_egw2ical[$partstat]; + + return array($atdval,$atdpars); + } + + + /** + * Make a value of type RECUR for a ical RRULE property + * + * A simple example: + * ( RRULE) : (FREQ=MONTHLY;COUNT=10;INTERVAL=2) + * here the first part between parenthesis is property and the + * second is a value of type RECUR + * + * @param string $recur_type the type of recurrence frequence we have + * @param mixed $recur_data Todo describe this parameter... + * @param int $recur_interval Todo describe this parameter... + * @param utime $recur_enddate the final date that the recurrence ends + * @return string ($recurval) a value format as RECUR for the RRULE property + * (if a time is set) + */ + function mki_v_RECUR($recur_type,$recur_data,$recur_interval,$recur_start,$recur_enddate) + { + $recur = array(); + $recurval ='FREQ=' . $this->recur_egw2ical[$recur_type]; + + switch ($recur_type) { + case MCAL_RECUR_WEEKLY: + $days = array(); + foreach($this->recur_days as $did => $day) { + if ($recur_data & $did) + $days[] = strtoupper(substr($day,0,2)); + } + $recur['BYDAY'] = implode(',',$days); + break; + case MCAL_RECUR_MONTHLY_MDAY: // date of the month: BYMONTDAY={1..31} + $recur['BYMONTHDAY'] = (int) date('d',$recur_start); + break; + case MCAL_RECUR_MONTHLY_WDAY: // weekday of the month: BDAY={1..5}{MO..SO} + $recur['BYDAY'] = (1 + (int) ((date('d',$recur_start)-1) / 7)) + . strtoupper(substr(date('l',$recur_start),0,2)); + break; + } + + if ($recur_interval > 1) + $recur['INTERVAL'] = $recur_interval; + + if ($recur_enddate){ + +// $expdt= $this->hi->_exportDateTime($recur_enddate); +// error_log('EXPORT UNTIL=' . $recur_enddate . ' expdDT:' . $expdt); + + $recur['UNTIL'] = $this->hi->_exportDateTime($recur_enddate); + } + foreach($recur as $parnam => $parval){ + $recurval .= ';' . $parnam . '=' . $parval; + } + return $recurval; + } + + /** + * Make a value (commasep string of dates) for the EXDATE property + * + * In the conversion you can chose between a commastring of DATES or DATE-TIMES + * @param array $recur_exceptions list with utime exception dates + * @param boolean $dtmode if true generate DATE-TIME dates else DATES + * @return array ($exdval, $exdparams) a list with the value and parameters generated + */ + function mki_vp_4EXDATE($recur_exceptions,$dtmode=false) + { + $days = array(); + foreach($recur_exceptions as $day) { + $days[] = date('Ymd',$day); + } + + $exdparams = array(); + if(!$dtmode) + $exdparams['VALUE'] = 'DATE'; + return array( implode(',',$days), $exdparams); + } + + /** + * Convert DDT possible DATE|DATE-TIME params and a value commalist + * into an array of utime dates. + * + * Some examples + *
    +	   * ex1: ...;VALUE=DATE:20060123,20060124
    +	   * ex2: ...:20060118T101500Z,20060119T1000Z
    +	   * ex3: ...:VALUE=DATE-TIME:20060118T101500Z,20060119T1000Z
    +	   * 
    + * @note unfortunately horde_icalendar will parse ex1 into an array of + * array(month => .. , mday => .. , year=> ) + * @param array $dvals list of dates + * @return array $udays list with the days from the input list in utime format + */ + function mke_EXDATEpv2udays($params, $dvals) + { + //$exdays = (!is_array($dvals)) ? $dvals : array($dvals); + $exdays = $dvals; + + if (count($exdays) < 1) + return false; +//error_log('EXDAYS params=' . print_r($params,true)); +//error_log('EXDAYS exploded=' . print_r($exdays,true)); + if($params['VALUE'] == 'DATE'){ + // list is in awful horde DATE mode + // convert the date somehow to udays + $udays = array(); + foreach ($exdays as $day){ + $udays[] = $this->mke_DDT2utime($day); + } + } else { + // assume list is in DT mode + $udays = &$exdays; + } + + return $udays; + } + + + + /** + * Convert a RECUR value into the corresponding egw recur fields. + * + * A value of type RECUR (for a ical RRULE property) is parsed + * into the 4 related egw fields. Fields unfilled stay false A + * simple example: ( RRULE) : + * (FREQ=MONTHLY;COUNT=10;INTERVAL=2) here the first + * part between parenthesis is property and the second is a + * value of type RECUR + * + * @bug RECUR: MONTHLY;BYMONTHDAY, only ok if startdate is also on this MONTHDAY + * egw problem. + * + * @todo RECUR: COUNT=xx;WEEKLY;BYDAY, may miss the last occurence, if not started + * on a BYDAY day: to be fixed! prio=low + * + * @todo RECUR: YEARLY seems only to support the most basic variant?? To be checked! + * + * @author JVL (required some thinking..) + * @param string $recur RECUR type value of RRULE + * @param mixed $rstart start date in UTC format + * @return array $rar a assoc array with keys: 'recur_type', 'recur_data', 'recur_interval' + * and 'recur_enddate'. On error: false + * @note the class var @ref $hi is used as auxiliary Horde_iCalendar object + */ + function mke_RECUR2rar($recur,$rstart) + { + $ustart = $this->mke_DDT2utime($rstart); + + // a6sd is in Icalsrv usertime + $a6sd = $this->utimetoa6($ustart); + +// error_log('IMPORT RECURVAL=' . $recur . 'ustart=' .$ustart); + + + $r_data = 0; + $dow =array(); // for weekly count calc + $r_type = $r_interval = $r_end = $r_count = false; + + $type = preg_match('/FREQ=([^;: ]+)/i',$recur,$matches) + ? $matches[1] : $recur[0]; + if ($type == false) + return false; + + // vCard 2.0 values for all types + if (preg_match('/UNTIL=([0-9TZ]+)/',$recur,$matches)) + $r_end = $this->hi->_parseDateTime($matches[1]); + + if (preg_match('/INTERVAL=([0-9]+)/',$recur,$matches)) + $r_interval = (int) $matches[1]; + + // with count given we must calculate r_end + if (preg_match('/COUNT=([0-9]+)/',$recur,$matches)){ + $r_count = (int) $matches[1]; + // count calculation auxvars + $c_interval = ($r_interval) ? $r_interval : 1; //interval + $c_count = ($r_count - 1)*$c_interval; + } + + switch($type) { + + case 'W': + case 'WEEKLY': + $days = array(); + if(preg_match('/W(\d+) (.*) (.*)/',$recur, $recurMatches)) { // 1.0 + $r_interval = $recurMatches[1]; + $c_interval = $r_interval; + $days = explode(' ',trim($recurMatches[2])); + } elseif (preg_match('/BYDAY=([^;: ]+)/',$recur,$recurMatches)) { // 2.0 + $days = explode(',',$recurMatches[1]); + } + if ($days) { + foreach($this->recur_days as $mid => $day) { + if (in_array(strtoupper(substr($day,0,2)), $days)){ //WAS ERROR IN BOICAL!! + $r_data |= $mid; + } + } + $r_type = MCAL_RECUR_WEEKLY; + } + // --------- r_end calculation from COUNT and BYDAYs --- + if ($r_count) { + $c_count = ($r_count - 1)*$c_interval; + foreach($days as $wdd){ + $dow[] = $this->dowseqid[$wdd]; + } + sort($dow); + $ustart_seqid = $this->dowseqid[strtoupper(substr(date('D',$ustart),0,2))]; + // find index of start day 0.. + $sdi = 0; //in case start is not on a byday + foreach($dow as $i) { + if ($dow[$i] == $ustart_seqid){ // hope start is on byday + $sdi = $i; break; + } elseif($dow[$i] >= $ustart_seqid){ // else next byday + $sdi = $i; break; + } + } + $edi = $sdi + $c_count; // end day index + $dur = 0; // duration until end in days + $wic = count($dow); // week indexes count + $dur = 7 * floor($edi / $wic) + $dow[($edi % $wic)]; + $dur -= $dow[$sdi]; + $a6sd['mday'] += intval($dur); + $r_end = $this->a6toutime($a6sd); + // destroy $a6sd here.. + +//error_log('count=' . $c_count .'sdi=' . $sdi . ' edi=' . $edi . +// ' wic=' .$wic . ' dur=' .$dur . ' r_end=' . $r_end); + } + break; + + case 'D': // 1.0 + if(!preg_match('/D(\d+) (.*)/',$recur, $recurMatches)) + break; + $c_interval = $r_interval = $recurMatches[1]; + $r_end = $this->hi->_parseDateTime($recurMatches[2]); + // fall-through + + case 'DAILY': // 2.0 + $r_type = MCAL_RECUR_DAILY; + if ($r_count) { // count calc is still experimental! + $c_count = ($r_count - 1)*$c_interval; + $a6sd['mday'] += $c_count; + $r_end = $this->a6toutime($a6sd); + } + break; + + case 'MONTHLY': + $r_type = strstr($recur,'BYDAY') ? + MCAL_RECUR_MONTHLY_WDAY : MCAL_RECUR_MONTHLY_MDAY; + // break; + //fall through + + case 'M': + if(preg_match('/MD(\d+) (.*)/',$recur, $recurMatches)) { + $r_type = MCAL_RECUR_MONTHLY_MDAY; + $c_interval = $r_interval = $recurMatches[1]; + } elseif(preg_match('/MP(\d+) (.*) (.*) (.*)/',$recur, $recurMatches)) { + $r_type = MCAL_RECUR_MONTHLY_WDAY; + $c_interval = $r_interval = $recurMatches[1]; + } + // + if ($r_count) { // count calc is still experimental! + switch ($r_type) { + case MCAL_RECUR_MONTHLY_MDAY: + // error_log('DOING MOTNHLY MDAY'); Egw doesnot handle this special, see todo + $c_count = ($r_count - 1)*$c_interval; // maybe changed because 1.0 found + $a6sd['month'] += $c_count; + $r_end = $this->a6toutime($a6sd); + break; + + case MCAL_RECUR_MONTHLY_WDAY: + $c_count = ($r_count - 1)*$c_interval; // maybe changed because 1.0 found + // startday + $dowsd = date('w',$this->a6toutime($a6sd)); // day of week for sd + //end day, first try + $a6ed = array_diff($a6sd,array()); $a6ed['month'] += $c_count; + //day1 of startmonth + $a6smd1 = array_diff($a6sd,array()); $a6smd1['mday'] = 1; + $dowsmd1 = date('w',$this->a6toutime($a6smd1)); // day of week for smd1 + + //startdate as day of 5week segment, anchored on and afer smd1 + $do5wsegsd = $dowsmd1 + $a6sd['mday']; + if($dowsmd1 > $dowsd) + $do5wsegsd -= 7; + + $a6ed['mday'] =1; + $dowemd1 = date('w',$this->a6toutime($a6ed)); // endmonthday1 as day of week + $a6ed['mday'] = $do5wsegsd - $dowemd1; + if($dowemd1 > $dowsd) + $a6ed['mday'] += 7; + +// error_log('dowsd='. $dowsd . ' dowsmd1='. $dowsmd1 . ' do5wsegsd=' . $do5wsegsd . +// ' dowemd1='. $dowemd1 . ' edmday=' . $a6ed['mday'] ); + $r_end = $this->a6toutime($a6ed); + break; + } + } + break; + + + case 'Y': // 1.0 + if(!preg_match('/YM(\d+) (.*)/',$recur, $recurMatches)) + break; + $c_interval = $r_interval = $recurMatches[1]; + // fall-through + + case 'YEARLY': // 2.0 + $r_type = MCAL_RECUR_YEARLY; + if ($r_count) { // count calc is still experimental! + // is there only this BYMONTHDAY support? + $c_count = ($r_count - 1)*$c_interval; // maybe changed because 1.0 found + $a6sd['year'] += $c_count; + $r_end = $this->a6toutime($a6sd); + } + break; + } + + return array('recur_type' => $r_type, + 'recur_data' => $r_data, + 'recur_interval' => $r_interval, + 'recur_enddate' => $r_end ); + } + + + /** + * Parse a CAL_ADDRESS and try to find the associated egw person_id + * @param string $attrval CAL_ADDRESS type value string + * @return int|false $pid associated (by email) egw pid. On error: false. + */ + function mke_CAL_ADDRESS2pid($attrval) + { + if (preg_match('/MAILTO:([@.a-z0-9_-]+)/i',$attrval,$matches) && + ($pid = $GLOBALS['egw']->accounts->name2id(strtolower($matches[1]),'account_email'))){ + return $pid; + } else { + return false; + } + } + + /** + * Parse a CAL_ADDRESS and PARAMS to find the CN name and email + * @param string $aval CAL_ADDRESS type value string + * @param array $aparams parameters for a ATTENDEE + * @return array $cneml assoc array with 'cn' and 'mailto' field + */ + function mke_ATTENDEE2cneml($aval,$aparams) + { + $cneml = array('cn' => '', 'mailto' => ''); + + // if (preg_match('/MAILTO:([@.a-z0-9_-]+)/i',$aval,$matches)) + // lets be a bit more relaxed here (rfc1378).. + // try for "Fnam Lnam " first + if (preg_match('/MAILTO:([^<]+)<([@.a-z0-9_-]+)>/i',$aval,$matches)){ + $cneml['cn'] = $matches[1]; + $cneml['mailto'] = $matches[2]; + // try for second + }elseif (preg_match('/MAILTO:([@.a-z0-9_-]+)/i',$aval,$matches)){ + $cneml['mailto'] = $matches[1]; + } + // a CN from the params overrules one from the mailto + if(isset($aparams['CN'])) + $cneml['cn'] = $aparams['CN']; + + return $cneml; + } + + /** + * Update an egw event description with a list of nonegw participants. + * + * note: this is a adhoc solution, preferably the nonegw participants + * should be added automatically to the addressbook + * @param string &$edescription the participants are append to this string as + * a string formatted ([cn: name:mailto: eml] [] ... ) + * @param array &$ne_participants array of the non egw participants as + * ('cn' =>, 'mailto' =>) pairs + * @return true + */ + function upde_nonegwParticipants2description(&$edescription,&$ne_participants) + { + $edescription.= "\n - non egw participants:\n("; + $neplist = array(); + foreach ($ne_participants as $nep){ +// $li = '[cn:' . $nep['cn']; +// $li .= ($nep['mailto']) ? ';mailto:' . $nep['mailto'] .']' : ']'; + $li = '[' . $nep['cn']; + $li .= ($nep['mailto']) ? '<' . $nep['mailto'] .'>]' : ']'; + + $neplist[]= $li; + } + $edescription .= implode("\n",$neplist) . ')'; + + return true; + } + + + /** + * Search a ical parameterlist for possible setting for a egw participant status. + * + * Parse the params array to find a PARTSTAT param, convert this to + * a egw partstatus (may occur e.g. in ATTENDEE params) + * @param array $params params of e.g. an ical ATTENDEE field + * @return array|false $epartstatus egw term for particpant status if detected else false + */ + function mke_params2partstat($params) + { + if (!isset($params['PARTSTAT'])) + return false; + + return $this->partstatus_ical2egw[strtoupper($params['PARTSTAT'])]; + } + + /** + * Add (append) an new attribute (aka field) to the vevent. + * + * @param horde_iCalendar_vevent $vevent obj to which the attribute is added + * @param string $aname name for the new attribute + * @param mixed $avalue value for the new attribute + * @param array $aparams parameters for the new attribute + * @return true + */ + function addAttributeOntoVevent(&$vevent,$aname,&$avalue,&$aparams) + { + + // it appears that translation->convert() can translate an array + // (that is: the values!, not the keys though) + // so lets apply it to the avalue and aparams, that should be enough! +// error_log('n:' . $aname . 'v:' . $avalue); + $valueData = + $GLOBALS['egw']->translation->convert($avalue, + $GLOBALS['egw']->translation->charset(), + 'UTF-8'); + $paramData = + $GLOBALS['egw']->translation->convert( $aparams, + $GLOBALS['egw']->translation->charset(), + 'UTF-8'); +// error_log('n:' . $aname . 'v:' . $valueData); + $vevent->setAttribute($aname, $valueData, $paramData); + $options = array(); + // JVL:is this really needed? + if (is_string($valueData)){ + +// // JVL: TEMPORARY SWITCHED OFF... TURN ON AGAIN!!! +// if(!(in_array($aname, array('RRULE'))) +// && preg_match('/([\000-\012\015\016\020-\037\075])/',$valueData)) { +// $options['ENCODING'] = 'QUOTED-PRINTABLE'; +// } + + if( (preg_match('/([\177-\377])/',$valueData))) { + $options['CHARSET'] = 'UTF-8'; + } + } + $vevent->setParameter($aname, $options); + + return true; + } + + + /** + * Convert egw alarm info to a ical VALARM object. + * + * Make a VALARM object form data in $alarms and $utstart (in utc) + * and with $vevent as container + * @param array &$alarm a single egw alarm array to be used + * @param horde_object &$vcomp that will be the container for the valarm + * mostly vevent or vtodo. + * @param array &$veExportFields list with fields that may get imported + * @return horde_iCalendar_valarm|false valarm object or, on error, false. + */ + function mki_c_VALARM(&$alarm, &$vcomp, $utstart,&$veExportFields){ + +// error_log('export comp-alarm-field:' . print_r($alarm,true)); + + $valarm = Horde_iCalendar::newComponent('VALARM',$vevent); + + //try first an offset + if($durtime = -$alarm['offset']){ + $valarm->setAttribute('TRIGGER', + $durtime, + array('VALUE' => 'DURATION', + 'RELATED' => 'START')); + // no success then try a date-time + } elseif($dtime = $alarm['time']){ + $valarm->setAttribute('TRIGGER', + $ddtime, + array('VALUE' => 'DATE-TIME')); + } else{ + $valarm = null; + return false; + } + $vcomp->addComponent($valarm); + + return $valarm; + } + + + /** + * Update the egw alarms array with info from a VALARM + * @param array &$alarms the the egw alarms array to be updated + * @param horde_iCalendar_valarm $valarm ref to the valarm component to be updated + * @param int $user_id the user that will own the alarms found + * @param array &$veImportFields with fields that may get imported + * @return true + */ + function upde_c_VALARM2alarms(&$alarms,&$valarm,$user_id,&$veImportFields){ + + // lets see what supported veImportFields we can get from the valarm + foreach($valarm->_attributes as $vattr) { + // $vattrval = $GLOBALS['egw']->translation->convert($vattr['value'],'UTF-8'); + // handle only supported fields + if(!in_array('VALARM/' . $vattr['name'], $veImportFields)) + continue; + + switch($vattr['name']) { + case 'TRIGGER': + $vtype = (isset($vattr['params']['VALUE'])) + ? $vattr['params']['VALUE'] : 'DURATION'; //default type + switch ($vtype) { + case 'DURATION': + $alarms[] = array('offset' => -$vattr['value']); + break; + case 'DATE-TIME': + $alarms[] = array('time' => $vattr['value']); + break; + default: + // we should also do ;RELATED=START|END + error_log('VALARM/TRIGGER: unsupported value type:' . $vtype); + } + break; +// case 'ACTION': +// break; +// case 'DISPLAY': +// break; + + default: + error_log('VALARM field:' .$vattr['name'] .':' + . print_r($vattrval,true) . ' HAS NO CONVERSION YET'); + } + } +// error_log('updated alarms to:' . print_r($alarms,true)); + return true; + } + + + + + + + } + +?> \ No newline at end of file diff --git a/egwical/inc/class.vcalinfolog.inc.compat.php b/egwical/inc/class.vcalinfolog.inc.compat.php new file mode 100644 index 0000000000..19d81d5694 --- /dev/null +++ b/egwical/inc/class.vcalinfolog.inc.compat.php @@ -0,0 +1,72 @@ +ei =& CreateObject('egwical.egwical'); + // $this->wkobj =& $this->ei->addRsc($this); + // or the fast, shortcut, road for knowingly experts only: + $this->wkobj =& CreateObject('egwical.boinfolog_vtodos'); + $this->wkobj->setRsc($this); + + if ($this->wkobj == false){ + error_log('boical constructor: couldnot add bocal resource to egwical: FATAL'); + return false; + } + } + + + // now implement the compatibility methods, that are all moved to egwical! + + function exportVTODO($_taskID, $_version) + { + return $this->wkobj->exportVTODO($_taskID, $_version); + } + + function importVTODO(&$_vcalData, $_taskID=-1) + { + return $this->wkobj->importVTODO($_vcalData, $_taskID); + } + + +// function setSupportedFields($_productManufacturer='file', $_productName='') +// { +// return $this->wkobj->setSupportedFields($_productManufacturer, $_productName); +// } + + } +?>