Imported Kolab_Format from Horde CVS.
authorGunnar Wrobel <p@rdus.de>
Tue, 23 Jun 2009 16:06:12 +0000 (18:06 +0200)
committerGunnar Wrobel <p@rdus.de>
Mon, 29 Jun 2009 03:54:57 +0000 (05:54 +0200)
31 files changed:
framework/Kolab_Format/COPYING [new file with mode: 0644]
framework/Kolab_Format/doc/Horde/Kolab/Format/usage.txt [new file with mode: 0644]
framework/Kolab_Format/examples/Horde/Kolab/Format/event.php [new file with mode: 0644]
framework/Kolab_Format/examples/Horde/Kolab/Format/new_type.php [new file with mode: 0644]
framework/Kolab_Format/lib/Horde/Kolab/Format.php [new file with mode: 0644]
framework/Kolab_Format/lib/Horde/Kolab/Format/Date.php [new file with mode: 0644]
framework/Kolab_Format/lib/Horde/Kolab/Format/XML.php [new file with mode: 0644]
framework/Kolab_Format/lib/Horde/Kolab/Format/XML/annotation.php [new file with mode: 0644]
framework/Kolab_Format/lib/Horde/Kolab/Format/XML/contact.php [new file with mode: 0644]
framework/Kolab_Format/lib/Horde/Kolab/Format/XML/distributionlist.php [new file with mode: 0644]
framework/Kolab_Format/lib/Horde/Kolab/Format/XML/event.php [new file with mode: 0644]
framework/Kolab_Format/lib/Horde/Kolab/Format/XML/hprefs.php [new file with mode: 0644]
framework/Kolab_Format/lib/Horde/Kolab/Format/XML/note.php [new file with mode: 0644]
framework/Kolab_Format/lib/Horde/Kolab/Format/XML/task.php [new file with mode: 0644]
framework/Kolab_Format/package.xml [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/AllTests.php [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/ContactTest.php [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/EventTest.php [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/MimeAttrTest.php [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/PreferencesTest.php [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/RecurrenceTest.php [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/XmlTest.php [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_category.xml [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_mail.xml [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_pgp.xml [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/event_umlaut.xml [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/event_umlaut_broken.xml [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/preferences_read_old.xml [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/preferences_write_old.xml [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/recur.xml [new file with mode: 0644]
framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/recur_fail.xml [new file with mode: 0644]

diff --git a/framework/Kolab_Format/COPYING b/framework/Kolab_Format/COPYING
new file mode 100644 (file)
index 0000000..d1c6b98
--- /dev/null
@@ -0,0 +1,504 @@
+                 GNU LESSER GENERAL PUBLIC LICENSE
+                      Version 2.1, February 1999
+
+ Copyright 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+\f
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+\f
+                 GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+\f
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+\f
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+\f
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+\f
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+\f
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+\f
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                           NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/framework/Kolab_Format/doc/Horde/Kolab/Format/usage.txt b/framework/Kolab_Format/doc/Horde/Kolab/Format/usage.txt
new file mode 100644 (file)
index 0000000..e459f6b
--- /dev/null
@@ -0,0 +1,339 @@
+The "Horde_Kolab_Format" package allows you to easily read and write
+the Kolab format within PHP.
+
+
+
+Installation of the package
+===========================
+
+The package is being distributed as a standard PEAR package by the
+Horde project. As long as you have PEAR installed, installation should
+be straigt forward.
+
+ pear channel-discover pear.horde.org
+ pear install --force channel://pear.horde.org/Horde_Kolab_Format
+
+"pear" will probably complain about the library (and its dependencies)
+not being marked stable yet, but the "--force" option allows to ignore
+these warnings.
+
+
+Using the package
+=================
+
+This section will present the construction of a short example script
+to demonstrate reading/writing an event in the Kolab XML format. The
+first required statement is the inclusion of the package:
+
+ require_once 'Horde/Kolab/Format.php';
+
+The API provided by the package is very simple. It only provides a
+"load()" and a "save()" function.
+
+In order to have access to these methods it is necessary to create the
+"Horde_Kolab_Format" object. The call looks like this:
+
+ $format = Horde_Kolab_Format::factory('XML', 'event');
+
+The function takes three arguments:
+
+# "Format type": Currently only "XML" is supported here.
+
+# "Object type": The type of object you want to read/write. The
+  package currently implements "contact", "distributionslist",
+  "event", "note", "task" and "hprefs"
+
+The <tt>$format</tt> variable created above now provides the means to
+save and load events in Kolab XML format. In order to save an event we
+need to prepare an array with all relevant information about this
+event:
+
+ $object = array(
+     'uid' => 1,
+     'summary' => 'test event',
+     'start-date' => time(),
+     'end-date' => time() + 24 * 60 * 60,
+ );
+
+This is an event that has the "UID" of "1" and carries the title "test
+event". It starts right now ("time()") and ends in a day ("time() + 24
+* 60 * 60").
+
+This event can now be saved using the "save()" function of the format
+handler:
+
+ $xml = $format->save($object);
+
+The function returns the Kolab XML format as a result. This string can
+be fed back into the "load()" function:
+
+ $read_object = $format->load($xml);
+
+If we dump the contents of the two variables <tt>$xml</tt> and
+<tt>$read_object</tt> this will be the result:
+
+ var_dump($xml);
+ string(438) "<?xml version="1.0"?>
+ <event version="1.0">
+   <uid>1</uid>
+   <body></body>
+   <categories></categories>
+   <creation-date>2008-07-10T12:51:51Z</creation-date>
+   <last-modification-date>2008-07-10T12:51:51Z</last-modification-date>
+   <sensitivity>public</sensitivity>
+   <product-id>Horde::Kolab</product-id>
+   <summary>test event</summary>
+   <start-date>2008-07-10T12:51:51Z</start-date>
+   <end-date>2008-07-11T12:51:51Z</end-date>
+ </event>
+ "
+
+ var_dump($read_object);
+ array(11) {
+   ["uid"]=>
+   string(1) "1"
+   ["body"]=>
+   string(0) ""
+   ["categories"]=>
+   string(0) ""
+   ["creation-date"]=>
+   int(1215694311)
+   ["last-modification-date"]=>
+   int(1215694311)
+   ["sensitivity"]=>
+   string(6) "public"
+   ["product-id"]=>
+   string(12) "Horde::Kolab"
+   ["summary"]=>
+   string(10) "test event"
+   ["start-date"]=>
+   int(1215694311)
+   ["attendee"]=>
+   array(0) {
+   }
+   ["end-date"]=>
+   int(1215780711)
+ }
+
+We see that the format stores a lot more information than we
+originally provided. The resulting XML string does not only contain
+the "uid", "summary", "start-date", and "end-date". Several additional
+attributes have been added. These were either calculated or set to a
+default value.
+
+* "body": holds the event description. We did not specify an event
+  description so this value has been set to an empty string.
+
+* "sensitivity": events may be "public" or "private" with "public"
+  being the default
+
+* "sensitivity": events may be "public" or "private" with "public"
+  being the default
+
+* "categories": Any Kolab object may be member of different
+  categories. As we didn't specify a category this value is also
+  empty.
+
+* "creation-date": The time stamp of the moment the object was
+  created.
+
+* "last-modification-date": The time stamp of the moment the object
+  was last modified.
+
+* "product-id": The ID of the product that last touched this
+  object. If we use the "Horde_Kolab_Format" package it will always be
+  "Horde::Kolab".
+
+If we read the XML data back into an array all these new informations
+are available within that array.
+
+
+
+Creating your own Kolab XML format
+==================================
+
+Currently the "Horde_Kolab_Format" implements the object types
+"contact", "distributionslist", "event", "note", "task" as they are
+defined within the Kolab Format specification. In addition the
+Horde specific "hprefs" type is available. It is used for storing
+Horde user preferences in the IMAP store provided by the Kolab server.
+
+Depending on the web application you might wish to connect with the
+Kolab server these object types may not be enough. Do not hesitate to
+define your own new type in that case. If you want it to find wider
+distribution you should of course discuss it on the Kolab Format
+mailing list (http://kolab.org/pipermail/kolab-format/) to get some
+feedback on the new type.
+
+The "Horde_Kolab_Format" packages makes the definition of a new object
+type rather straight forward. The following will explain the creation
+of a very simple new object that only saves a single string value.
+
+This time it will be necessary to load the XML format definition,
+too. Any new object type will extend this XML definition:
+
+ require_once 'Horde/Kolab/Format.php';
+ require_once 'Horde/Kolab/Format/XML.php';
+
+A new object type is represented by a class that extends
+"Horde_Kolab_Format_XML":
+
+ class Horde_Kolab_Format_XML_string extends Horde_Kolab_Format_XML {
+
+     var $_fields_specific;
+
+     function Horde_Kolab_Format_XML_string()
+     {
+         $this->_root_name = 'string';
+
+         /** Specific fields of this object type                           
+          */
+         $this->_fields_specific = array(
+             'string' => array(
+                 'type' => HORDE_KOLAB_XML_TYPE_STRING,
+                 'value' => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+             ),
+         );
+
+         parent::Horde_Kolab_Format_XML();
+     }
+ }
+
+The class needs to end with the name of the object type. Here it is
+just "string".
+
+The declaration "var $_fields_specific;" indicates that the new object
+type has attributes beyond the basic set required for any Kolab
+object. So this part may not be missing for a declaration of a new
+type.
+
+The function creating the class ("Horde_Kolab_Format_XML_string()")
+needs to do three things:
+
+* Declaring the XML root name which will be "string" here. It should
+  always match the type name.
+
+* Declaring the specific attributes of the object. This part populates
+  the "_fields_specific" variable with an array describing the
+  possible object attributes. This will be described in more detail
+  further below.
+
+* Calling the parent constructor using
+  "parent::Horde_Kolab_Format_XML()".
+
+The new format can now be used as demonstrated in the initial event
+example:
+
+ $format = Horde_Kolab_Format::factory('XML', 'string');
+ $object = array(
+     'uid' => 1,
+     'string' => 'test string',
+ );
+ $xml = $format->save($object);
+ $read_object = $format->load($xml);
+ var_dump($xml);
+ var_dump($read_object);
+
+The result looks like this:
+
+ string(347) "<?xml version="1.0"?>
+ <string version="1.0">
+   <uid>1</uid>
+   <body></body>
+   <categories></categories>
+   <creation-date>2008-07-10T13:28:36Z</creation-date>
+   <last-modification-date>2008-07-10T13:28:36Z</last-modification-date>
+   <sensitivity>public</sensitivity>
+   <product-id>Horde::Kolab</product-id>
+   <string>test string</string>
+ </string>
+ "
+
+ array(8) {
+   ["uid"]=>
+   string(1) "1"
+   ["body"]=>
+   string(0) ""
+   ["categories"]=>
+   string(0) ""
+   ["creation-date"]=>
+   int(1215696516)
+   ["last-modification-date"]=>
+   int(1215696516)
+   ["sensitivity"]=>
+   string(6) "public"
+   ["product-id"]=>
+   string(12) "Horde::Kolab"
+   ["string"]=>
+   string(11) "test string"
+ }
+
+
+
+Allowed fields
+==============
+
+There are only a number of valid entries available to specify the
+attributes a new object type may contain.
+
+Each entry in the field list will look like this
+
+ 'attribute_name' => array(
+     'type' => HORDE_KOLAB_XML_TYPE_*,
+     'value' => HORDE_KOLAB_XML_VALUE_*,
+ ),
+
+"attribute_name" should be a short name describing the value that
+should be stored. "type" must be set to one of the following
+"HORDE_KOLAB_XML_TYPE_*" type values:
+
+* "HORDE_KOLAB_XML_TYPE_STRING": A string.
+
+* "HORDE_KOLAB_XML_TYPE_INTEGER": A number
+
+* "HORDE_KOLAB_XML_TYPE_BOOLEAN": True or false.
+
+* "HORDE_KOLAB_XML_TYPE_DATE": A date (e.g. 2008/08/08)
+
+* "HORDE_KOLAB_XML_TYPE_DATETIME": A time and a date.
+
+* "HORDE_KOLAB_XML_TYPE_DATE_OR_DATETIME": A date or a time and a
+  date.
+
+* "HORDE_KOLAB_XML_TYPE_COLOR": A color (#00BBFF).
+
+* "HORDE_KOLAB_XML_TYPE_COMPOSITE": A composite element that combines
+  several attributes.
+
+* "HORDE_KOLAB_XML_TYPE_MULTIPLE": Wrapper for an element that may
+  occur several times.
+
+Examples for "HORDE_KOLAB_XML_TYPE_COMPOSITE" and
+"HORDE_KOLAB_XML_TYPE_MULTIPLE" can be found in the definitions
+currently provided by the "Horde_Kolab_Format" package.
+
+The following "value" settings are allowed:
+
+* "HORDE_KOLAB_XML_VALUE_DEFAULT": An attribute with a default value.
+
+* "HORDE_KOLAB_XML_VALUE_MAYBE_MISSING": An attribute that may be left
+  undefined.
+
+* "HORDE_KOLAB_XML_VALUE_NOT_EMPTY": An attribute that will cause an
+  error if it is left undefined.
+
+* "HORDE_KOLAB_XML_VALUE_CALCULATE": A complex attribute that gets its
+  own function for calculating the correct value.
+
+Examples for "HORDE_KOLAB_XML_VALUE_CALCULATE" can again be found in
+the current object types implemented in "Horde_Kolab_Format".
+
+
+Detailed package documentation
+==============================
+
+A detailed documentation based on the code comments and extracted via
+phpDocumentor can be found at
+http://dev.horde.org/api/framework/. Simply select the package
+"Horde_Kolab_Format" in the package selection box in the upper right
+corner.
diff --git a/framework/Kolab_Format/examples/Horde/Kolab/Format/event.php b/framework/Kolab_Format/examples/Horde/Kolab/Format/event.php
new file mode 100644 (file)
index 0000000..f3bb6cb
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+/**
+ * A sample script for reading/writing an event.
+ *
+ * $Horde: framework/Kolab_Format/examples/Horde/Kolab/Format/event.php,v 1.3 2008/08/01 07:04:52 wrobel Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/** We need the Horde_Kolab_Format package */
+require_once 'Horde/Kolab/Format.php';
+
+/** Generate the format handler */
+$format = Horde_Kolab_Format::factory('XML', 'event');
+
+/** Prepare a test object */
+$object = array(
+    'uid' => 1,
+    'summary' => 'test event',
+    'start-date' => time(),
+    'end-date' => time() + 24 * 60 * 60,
+);
+
+/** Save this test data array in Kolab XML format */
+$xml = $format->save($object);
+var_dump($xml);
+
+/** Reload the object from the XML format */
+$read_object = $format->load($xml);
+var_dump($read_object);
+
diff --git a/framework/Kolab_Format/examples/Horde/Kolab/Format/new_type.php b/framework/Kolab_Format/examples/Horde/Kolab/Format/new_type.php
new file mode 100644 (file)
index 0000000..1cd2a9f
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+/**
+ * An example of defining a new Kolab format type
+ *
+ * $Horde: framework/Kolab_Format/examples/Horde/Kolab/Format/new_type.php,v 1.4 2009/01/06 17:49:22 jan Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/** We need the Horde_Kolab_Format package */
+require_once 'Horde/Kolab/Format.php';
+
+/** And we need the XML definition */
+require_once 'Horde/Kolab/Format/XML.php';
+
+/**
+ * Kolab XML handler for a string value
+ *
+ * $Horde: framework/Kolab_Format/examples/Horde/Kolab/Format/new_type.php,v 1.4 2009/01/06 17:49:22 jan Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_XML_string extends Horde_Kolab_Format_XML {
+
+    /**
+     * Specific data fields for the prefs object
+     *
+     * @var Kolab
+     */
+    var $_fields_specific;
+
+    /**
+     * Constructor
+     */
+    function Horde_Kolab_Format_XML_string()
+    {
+        $this->_root_name = 'string';
+
+        /** Specific preferences fields, in kolab format specification order
+         */
+        $this->_fields_specific = array(
+            'string' => array(
+                'type' => HORDE_KOLAB_XML_TYPE_STRING,
+                'value' => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+        );
+
+        parent::Horde_Kolab_Format_XML();
+    }
+}
+
+/** Generate the format handler */
+$format = Horde_Kolab_Format::factory('XML', 'string');
+
+/** Prepare a test object */
+$object = array(
+    'uid' => 1,
+    'string' => 'test string',
+);
+
+/** Save this test data array in Kolab XML format */
+$xml = $format->save($object);
+var_dump($xml);
+
+/** Reload the object from the XML format */
+$read_object = $format->load($xml);
+var_dump($read_object);
+
diff --git a/framework/Kolab_Format/lib/Horde/Kolab/Format.php b/framework/Kolab_Format/lib/Horde/Kolab/Format.php
new file mode 100644 (file)
index 0000000..d2dd7e5
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/**
+ * A library for reading/writing the Kolab format.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format.php,v 1.7 2008/12/12 11:25:52 wrobel Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/** We need PEAR */
+require_once 'PEAR.php';
+
+/**
+ * The Horde_Kolab_Format:: class provides the means to read/write the
+ * Kolab format.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format.php,v 1.7 2008/12/12 11:25:52 wrobel Exp $
+ *
+ * Copyright 2007-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @since   Horde 3.2
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format
+{
+
+    /**
+     * Attempts to return a concrete Horde_Kolab_Format instance based on $format_type.
+     *
+     * @param string    $format_type    The format type that should be handled.
+     * @param string    $object_type    The object type that should be handled.
+     * @param array     $params         An array of  additional parameters.
+     *
+     *                                  Supported parameters:
+     *
+     *                                    'version' - The format version.
+     *
+     * @return mixed    The newly created concrete Horde_Kolab_Format_XML instance, or
+     *                  a PEAR error.
+     */
+    function &factory($format_type = '', $object_type = '', $params = null)
+    {
+        @include_once dirname(__FILE__) . '/Format/' . $format_type . '.php';
+        $class = 'Horde_Kolab_Format_' . $format_type;
+        if (class_exists($class)) {
+            $driver = call_user_func(array($class, 'factory'), $object_type, $params);
+        } else {
+            return PEAR::raiseError(sprintf(_("Failed to load Kolab Format driver %s"), $format_type));
+        }
+
+        return $driver;
+    }
+
+}
diff --git a/framework/Kolab_Format/lib/Horde/Kolab/Format/Date.php b/framework/Kolab_Format/lib/Horde/Kolab/Format/Date.php
new file mode 100644 (file)
index 0000000..8a1a622
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+/**
+ * Helper functions to handle format conversions.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/Date.php,v 1.6 2009/01/06 17:49:22 jan Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ * Kolab date handling functions. Based upon Kolab.php from Stuart Binge.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/Date.php,v 1.6 2009/01/06 17:49:22 jan Exp $
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Stuart Binge <omicron@mighty.co.za>
+ * @author  Thomas Jarosch <thomas.jarosch@intra2net.com>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_Date
+{
+    /**
+     * Returns a UNIX timestamp corresponding the given date string which is in
+     * the format prescribed by the Kolab Format Specification.
+     *
+     * @param string $date  The string representation of the date.
+     *
+     * @return integer  The unix timestamp corresponding to $date.
+     */
+    function decodeDate($date)
+    {
+        if (empty($date)) {
+            return 0;
+        }
+
+        list($year, $month, $day) = explode('-', $date);
+
+        return mktime(0, 0, 0, $month, $day, $year);
+    }
+
+    /**
+     * Returns a UNIX timestamp corresponding the given date-time string which
+     * is in the format prescribed by the Kolab Format Specification.
+     *
+     * @param string $datetime  The string representation of the date & time.
+     *
+     * @return integer  The unix timestamp corresponding to $datetime.
+     */
+    function decodeDateTime($datetime)
+    {
+        if (empty($datetime)) {
+            return 0;
+        }
+
+        list($year, $month, $day, $hour, $minute, $second)
+            = sscanf($datetime, '%d-%d-%dT%d:%d:%dZ');
+        return gmmktime($hour, $minute, $second, $month, $day, $year);
+    }
+
+    /**
+     * Returns a UNIX timestamp corresponding the given date or date-time
+     * string which is in either format prescribed by the Kolab Format
+     * Specification.
+     *
+     * @param string $date  The string representation of the date (& time).
+     *
+     * @return integer  The unix timestamp corresponding to $date.
+     */
+    function decodeDateOrDateTime($date)
+    {
+        if (empty($date)) {
+            return 0;
+        }
+
+        return (strlen($date) == 10 ? Horde_Kolab_Format_Date::decodeDate($date) : Horde_Kolab_Format_Date::decodeDateTime($date));
+    }
+
+    /**
+     * Returns a string containing the current UTC date in the format
+     * prescribed by the Kolab Format Specification.
+     *
+     * @return string  The current UTC date in the format 'YYYY-MM-DD'.
+     */
+    function encodeDate($date = false)
+    {
+        if ($date === false) {
+            $date = time();
+        }
+
+        return strftime('%Y-%m-%d', $date);
+    }
+
+    /**
+     * Returns a string containing the current UTC date and time in the format
+     * prescribed by the Kolab Format Specification.
+     *
+     * @return string    The current UTC date and time in the format
+     *                   'YYYY-MM-DDThh:mm:ssZ', where the T and Z are literal
+     *                   characters.
+     */
+    function encodeDateTime($datetime = false)
+    {
+        if ($datetime === false) {
+            $datetime = time();
+        }
+
+        return gmstrftime('%Y-%m-%dT%H:%M:%SZ', $datetime);
+    }
+}
diff --git a/framework/Kolab_Format/lib/Horde/Kolab/Format/XML.php b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML.php
new file mode 100644 (file)
index 0000000..755e9a6
--- /dev/null
@@ -0,0 +1,1468 @@
+<?php
+/**
+ * Implementation of the Kolab XML format.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML.php,v 1.16 2009/04/08 18:32:15 wrobel Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/** We need the DOM library for XML access. */
+require_once 'Horde/DOM.php';
+
+/** We need the String library for string conversions. */
+require_once 'Horde/String.php';
+
+/** We need the NLS library for language support. */
+require_once 'Horde/NLS.php';
+
+/** Kolab date handling functions. */
+require_once 'Horde/Kolab/Format/Date.php';
+
+/**
+ * Defines a XML value that should get a default value if missing
+ */
+define('HORDE_KOLAB_XML_PRODUCT_ID', 'Horde::Kolab');
+
+/**
+ * Defines a XML value that should get a default value if missing
+ */
+define('HORDE_KOLAB_XML_VALUE_DEFAULT', 0);
+
+/**
+ * Defines a XML value that may be missing
+ */
+define('HORDE_KOLAB_XML_VALUE_MAYBE_MISSING', 1);
+
+/**
+ * Defines a XML value that may not be missing
+ */
+define('HORDE_KOLAB_XML_VALUE_NOT_EMPTY', 2);
+
+/**
+ * Defines a XML value that will be calculated by its own function
+ */
+define('HORDE_KOLAB_XML_VALUE_CALCULATED', 3);
+
+/**
+ * Defines a XML value as string type
+ */
+define('HORDE_KOLAB_XML_TYPE_STRING', 0);
+
+/**
+ * Defines a XML value as integer type
+ */
+define('HORDE_KOLAB_XML_TYPE_INTEGER', 1);
+
+/**
+ * Defines a XML value as boolean type
+ */
+define('HORDE_KOLAB_XML_TYPE_BOOLEAN', 2);
+
+/**
+ * Defines a XML value as date type
+ */
+define('HORDE_KOLAB_XML_TYPE_DATE', 3);
+
+/**
+ * Defines a XML value as datetime type
+ */
+define('HORDE_KOLAB_XML_TYPE_DATETIME', 4);
+
+/**
+ * Defines a XML value as date or datetime type
+ */
+define('HORDE_KOLAB_XML_TYPE_DATE_OR_DATETIME', 5);
+
+/**
+ * Defines a XML value as color type
+ */
+define('HORDE_KOLAB_XML_TYPE_COLOR', 6);
+
+/**
+ * Defines a XML value as composite value type
+ */
+define('HORDE_KOLAB_XML_TYPE_COMPOSITE', 7);
+
+/**
+ * Defines a XML value as array type
+ */
+define('HORDE_KOLAB_XML_TYPE_MULTIPLE', 8);
+
+/**
+ * Kolab XML to array hash converter.
+ *
+ * For implementing a new format type you will have to inherit this
+ * class and provide a _load/_save function.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML.php,v 1.16 2009/04/08 18:32:15 wrobel Exp $
+ *
+ * Copyright 2007-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @since   Horde 3.2
+ * @author  Thomas Jarosch <thomas.jarosch@intra2net.com>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_XML
+{
+    /**
+     * Requested version of the data array to return
+     *
+     * @var int
+     */
+    var $_version = 1;
+
+    /**
+     * The name of the resulting document.
+     *
+     * @var string
+     */
+    var $_name = 'kolab.xml';
+
+    /**
+     * The XML document this driver works with.
+     *
+     * @var Horde_DOM_Document
+     */
+        var $_xmldoc = null;
+
+    /**
+     * The name of the root element.
+     *
+     * @var string
+     */
+    var $_root_name = 'kolab';
+
+    /**
+     * Kolab format version of the root element.
+     *
+     * @var string
+     */
+    var $_root_version = '1.0';
+
+    /**
+     * Basic fields in any Kolab object
+     *
+     * @var array
+     */
+    var $_fields_basic;
+
+    /**
+     * Automatically create categories if they are missing?
+     *
+     * @var boolean
+     */
+    var $_create_categories = true;
+
+    /**
+     * Fields for a simple person
+     *
+     * @var array
+     */
+    var $_fields_simple_person = array(
+        'type'    => HORDE_KOLAB_XML_TYPE_COMPOSITE,
+        'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        'array'   => array(
+            'display-name' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => '',
+            ),
+            'smtp-address' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => '',
+            ),
+            'uid' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => '',
+            ),
+        ),
+    );
+
+    /**
+     * Fields for an attendee
+     *
+     * @var array
+     */
+    var $_fields_attendee = array(
+        'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+        'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+        'default' => array(),
+        'array'   => array(
+            'type'    => HORDE_KOLAB_XML_TYPE_COMPOSITE,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            'array'   => array(
+                'display-name' => array(
+                    'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                    'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                    'default' => '',
+                ),
+                'smtp-address' => array(
+                    'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                    'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                    'default' => '',
+                ),
+                'status' => array(
+                    'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                    'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                    'default' => 'none',
+                ),
+                'request-response' => array(
+                    'type'    => HORDE_KOLAB_XML_TYPE_BOOLEAN,
+                    'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                    'default' => true,
+                ),
+                'role' => array(
+                    'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                    'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                    'default' => 'required',
+                ),
+            ),
+        ),
+    );
+
+    /**
+     * Fields for a recurrence
+     *
+     * @var array
+     */
+    var $_fields_recurrence = array(
+        // Attribute on root node: cycle
+        // Attribute on root node: type
+        'interval' => array(
+            'type'    => HORDE_KOLAB_XML_TYPE_INTEGER,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        ),
+        'day' => array(
+            'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            'array'   => array(
+                'type' => HORDE_KOLAB_XML_TYPE_STRING,
+                'value' => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+        ),
+        'daynumber' => array(
+            'type'    => HORDE_KOLAB_XML_TYPE_INTEGER,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        ),
+        'month' => array(
+            'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        ),
+        // Attribute on range: type
+        'range' => array(
+            'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+            'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+            'default' => '',
+        ),
+        'exclusion' => array(
+            'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            'array'   => array(
+                'type' => HORDE_KOLAB_XML_TYPE_STRING,
+                'value' => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+        ),
+        'complete' => array(
+            'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            'array'   => array(
+                'type' => HORDE_KOLAB_XML_TYPE_STRING,
+                'value' => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+        ),
+    );
+
+   /**
+     * Constructor
+     *
+     * @param array       $params        Any additional options
+     */
+    function Horde_Kolab_Format_XML($params = null)
+    {
+        if (is_array($params) && isset($params['version'])) {
+            $this->_version = $params['version'];
+        } else {
+            $this->_version = 1;
+        }
+
+        /* Generic fields, in kolab format specification order */
+        $this->_fields_basic = array(
+            'uid' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
+            ),
+            'body' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => '',
+            ),
+            'categories' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => '',
+            ),
+            'creation-date' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_DATETIME,
+                'value'   => HORDE_KOLAB_XML_VALUE_CALCULATED,
+                'load'    => 'CreationDate',
+                'save'    => 'CreationDate',
+            ),
+            'last-modification-date' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_DATETIME,
+                'value'   => HORDE_KOLAB_XML_VALUE_CALCULATED,
+                'load'    => 'ModificationDate',
+                'save'    => 'ModificationDate',
+            ),
+            'sensitivity' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => 'public',
+            ),
+            'inline-attachment' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                'array'   => array(
+                    'type'  => HORDE_KOLAB_XML_TYPE_STRING,
+                    'value' => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                ),
+            ),
+            'link-attachment' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                'array'   => array(
+                    'type'  => HORDE_KOLAB_XML_TYPE_STRING,
+                    'value' => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                ),
+            ),
+            'product-id' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_CALCULATED,
+                'load'    => 'ProductId',
+                'save'    => 'ProductId',
+            ),
+        );
+    }
+
+    /**
+     * Attempts to return a concrete Horde_Kolab_Format_XML instance.
+     * based on $object_type.
+     *
+     * @param string    $object_type    The object type that should be handled.
+     * @param array     $params         Any additional parameters.
+     *
+     * @return Horde_Kolab_Format_XML|PEAR_Error The newly created concrete
+     *                                           Horde_Kolab_Format_XML
+     *                                           instance.
+     */
+    function &factory($object_type = '', $params = null)
+    {
+        $object_type = str_replace('-', '', $object_type);
+        @include_once dirname(__FILE__) . '/XML/' . $object_type . '.php';
+        $class = 'Horde_Kolab_Format_XML_' . $object_type;
+
+        if (class_exists($class)) {
+            $driver = &new $class($params);
+        } else {
+            return PEAR::raiseError(sprintf(_("Failed to load Kolab XML driver %s"), $object_type));
+        }
+
+        return $driver;
+    }
+
+    /**
+     * Return the name of the resulting document.
+     *
+     * @return string The name that may be used as filename.
+     */
+    function getName()
+    {
+        return $this->_name;
+    }
+
+    /**
+     * Return the mime type of the resulting document.
+     *
+     * @return string The mime type of the result.
+     */
+    function getMimeType()
+    {
+        return 'application/x-vnd.kolab.' . $this->_root_name;
+    }
+
+    /**
+     * Return the disposition of the resulting document.
+     *
+     * @return string The disportion of this document.
+     */
+    function getDisposition()
+    {
+        return 'attachment';
+    }
+
+    /**
+     * Load an object based on the given XML string.
+     *
+     * @todo Check encoding of the returned array. It seems to be ISO-8859-1 at
+     * the moment and UTF-8 would seem more appropriate.
+     *
+     * @param string $xmltext  The XML of the message as string.
+     *
+     * @return array|PEAR_Error The data array representing the object.
+     */
+    function load(&$xmltext)
+    {
+        $noderoot = $this->_parseXml($xmltext);
+        if (is_a($noderoot, 'PEAR_Error') || empty($noderoot)) {
+            /**
+             * If the first call does not return successfully this might mean we
+             * got an attachment with broken encoding. There are some Kolab
+             * client versions in the wild that might have done that. So the
+             * next section starts a second attempt by guessing the encoding and
+             * trying again.
+             */
+            if (strcasecmp(mb_detect_encoding($xmltext, 'UTF-8, ISO-8859-1'), 'UTF-8') !== 0) {
+                $xmltext = mb_convert_encoding($xmltext, 'UTF-8', 'ISO-8859-1');
+            }
+            $noderoot = $this->_parseXml($xmltext);
+        }
+
+        if (is_a($noderoot, 'PEAR_Error')) {
+            return $noderoot;
+        }
+        if (empty($noderoot)) {
+            return false;
+        }
+
+        if (!$noderoot->has_child_nodes()) {
+            return PEAR::raiseError(_("No or unreadable content in Kolab XML object"));
+        }
+
+        $children = $noderoot->child_nodes();
+
+        // fresh object data
+        $object = array();
+
+        $result = $this->_loadArray($children, $this->_fields_basic);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        $object = array_merge($object, $result);
+        $this->_loadMultipleCategories($object);
+
+        $result = $this->_load($children);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        $object = array_merge($object, $result);
+
+        // uid is vital
+        if (!isset($object['uid'])) {
+            return PEAR::raiseError(_("UID not found in Kolab XML object"));
+        }
+
+        return $object;
+    }
+
+    /**
+     * Load the groupware object based on the specifc XML values.
+     *
+     * @access protected
+     *
+     * @param array $children An array of XML nodes.
+     *
+     * @return array|PEAR_Error The data array representing the object.
+     *
+     */
+    function _load(&$children)
+    {
+        if (!empty($this->_fields_specific)) {
+            return $this->_loadArray($children, $this->_fields_specific);
+        } else {
+            return array();
+        }
+    }
+
+    /**
+     * Load an array with data from the XML nodes.
+     *
+     * @access private
+     *
+     * @param array $object   The resulting data array.
+     * @param array $children An array of XML nodes.
+     * @param array $fields   The fields to populate in the object array.
+     *
+     * @return boolean|PEAR_Error True on success.
+     */
+    function _loadArray(&$children, $fields)
+    {
+        $object = array();
+
+        // basic fields below the root node
+        foreach($fields as $field => $params) {
+            $result = $this->_getXmlData($children, $field, $params);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+            if (isset($result)) {
+                $object[$field] = $result;
+            }
+        }
+        return $object;
+    }
+
+    /**
+     * Get the text content of the named data node among the specified
+     * children.
+     *
+     * @access private
+     *
+     * @param array  $children     The children to search.
+     * @param string $name         The name of the node to return.
+     * @param array  $params       Parameters for the data conversion.
+     *
+     * @return string|PEAR_Error The content of the specified node or an empty
+     *                           string.
+     */
+    function _getXmlData($children, $name, $params)
+    {
+        $value = null;
+        $missing = false;
+
+        // identify the child node
+        $child = $this->_findNode($children, $name);
+
+        // Handle empty values
+        if (!$child) {
+            if ($params['value'] == HORDE_KOLAB_XML_VALUE_MAYBE_MISSING) {
+                // 'MAYBE_MISSING' means we should return null
+                return null;
+            } elseif ($params['value'] == HORDE_KOLAB_XML_VALUE_NOT_EMPTY) {
+                // May not be empty. Return an error
+                return PEAR::raiseError(sprintf(_("Data value for %s is empty in Kolab XML object!"), $name));
+            } elseif ($params['value'] == HORDE_KOLAB_XML_VALUE_DEFAULT) {
+                // Return the default
+                return $params['default'];
+            } elseif ($params['value'] == HORDE_KOLAB_XML_VALUE_CALCULATED) {
+                $missing = true;
+            }
+        }
+
+        // Do we need to calculate the value?
+        if ($params['value'] == HORDE_KOLAB_XML_VALUE_CALCULATED && isset($params['load'])) {
+            if (method_exists($this, '_load' . $params['load'])) {
+                $value = call_user_func(array($this, '_load' . $params['load']),
+                                        $child, $missing);
+            } else {
+                return PEAR::raiseError(sprintf("Kolab XML: Missing function %s!", $params['load']));
+            }
+        } elseif ($params['type'] == HORDE_KOLAB_XML_TYPE_COMPOSITE) {
+            return $this->_loadArray($child->child_nodes(), $params['array']);
+        } elseif ($params['type'] == HORDE_KOLAB_XML_TYPE_MULTIPLE) {
+            $result = array();
+            foreach($children as $child) {
+                if ($child->type == XML_ELEMENT_NODE && $child->tagname == $name) {
+                    $value = $this->_getXmlData(array($child), $name, $params['array']);
+                    if (is_a($value, 'PEAR_Error')) {
+                        return $value;
+                    }
+                    $result[] = $value;
+                }
+            }
+            return $result;
+        } else {
+            return $this->_loadDefault($child, $params);
+        }
+
+        // Nothing specified. Return the value as it is.
+        return $value;
+    }
+
+    /**
+     * Parse the XML string. The root node is returned on success.
+     *
+     * @access private
+     *
+     * @param string $xmltext  The XML of the message as string.
+     *
+     * @return Horde_DOM_Node|PEAR_Error  The root node of the document.
+     */
+    function _parseXml(&$xmltext)
+    {
+        $params = array(
+            'xml' => $xmltext,
+            'options' => HORDE_DOM_LOAD_REMOVE_BLANKS,
+        );
+
+        $result = Horde_DOM_Document::factory($params);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        $this->_xmldoc = $result;
+        return $this->_xmldoc->document_element();
+    }
+
+    /**
+     * Convert the data to a XML string.
+     *
+     * @param array $attributes  The data array representing the note.
+     *
+     * @return string|PEAR_Error The data as XML string.
+     */
+    function save($object)
+    {
+        $root = $this->_prepareSave();
+
+        $this->_saveMultipleCategories($object);
+        $result = $this->_saveArray($root, $object, $this->_fields_basic);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $result = $this->_save($root, $object);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        return $this->_xmldoc->dump_mem(true);
+    }
+
+    /**
+     * Save the specific XML values.
+     *
+     * @access protected
+     *
+     * @param array $root     The XML document root.
+     * @param array $object   The resulting data array.
+     *
+     * @return boolean|PEAR_Error True on success.
+     */
+    function _save($root, $object)
+    {
+        if (!empty($this->_fields_specific)) {
+            $result = $this->_saveArray($root, $object, $this->_fields_specific);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Creates a new XML document if necessary.
+     *
+     * @access private
+     *
+     * @param string $xmltext  The XML of the message as string.
+     *
+     * @return Horde_DOM_Node The root node of the document.
+     */
+    function _prepareSave()
+    {
+        if ($this->_xmldoc != null) {
+            $root = $this->_xmldoc->document_element();
+        } else {
+            // create new XML
+            $this->_xmldoc = Horde_DOM_Document::factory();
+            $root = $this->_xmldoc->create_element($this->_root_name);
+            $root = $this->_xmldoc->append_child($root);
+            $root->set_attribute("version", $this->_root_version);
+        }
+
+        return $root;
+    }
+
+    /**
+     * Save a data array to XML nodes.
+     *
+     * @access private
+     *
+     * @param array   $root     The XML document root.
+     * @param array   $object   The data array.
+     * @param array   $fields   The fields to write into the XML object.
+     * @param boolean $append   Should the nodes be appended?
+     *
+     * @return boolean|PEAR_Error True on success.
+     */
+    function _saveArray($root, $object, $fields, $append = false)
+    {
+        // basic fields below the root node
+        foreach($fields as $field => $params) {
+            $result = $this->_updateNode($root, $object, $field, $params, $append);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Update the specified node.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node $parent_node  The parent node of the node that
+     *                                     should be updated.
+     * @param array          $attributes   The data array that holds all
+     *                                     attribute values.
+     * @param string         $name         The name of the the attribute
+     *                                     to be updated.
+     * @param array          $params       Parameters for saving the node
+     * @param boolean        $append       Should the node be appended?
+     *
+     * @return Horde_DOM_Node|PEAR_Error The new/updated child node.
+     */
+    function _updateNode($parent_node, $attributes, $name, $params, $append = false)
+    {
+        $value = null;
+        $missing = false;
+
+        // Handle empty values
+        if (!isset($attributes[$name])) {
+            // Do we have information if this may be empty?
+            if ($params['value'] == HORDE_KOLAB_XML_VALUE_DEFAULT) {
+                // Use the default
+                $value = $params['default'];
+            } elseif ($params['value'] == HORDE_KOLAB_XML_VALUE_NOT_EMPTY) {
+                // May not be empty. Return an error
+                return PEAR::raiseError(sprintf(_("Data value for %s is empty in Kolab XML object!"), $name));
+            } elseif ($params['value'] == HORDE_KOLAB_XML_VALUE_MAYBE_MISSING) {
+                /**
+                 * 'MAYBE_MISSING' means we should not create an XML
+                 * node here
+                 */
+                $this->_removeNodes($parent_node, $name);
+                return false;
+            } elseif ($params['value'] == HORDE_KOLAB_XML_VALUE_CALCULATED) {
+                $missing = true;
+            }
+        } else {
+            $value = $attributes[$name];
+        }
+
+        if ($params['value'] == HORDE_KOLAB_XML_VALUE_CALCULATED) {
+            // Calculate the value
+            if (method_exists($this, '_save' . $params['save'])) {
+                return call_user_func(array($this, '_save' . $params['save']),
+                                      $parent_node, $name, $value, $missing);
+            } else {
+                return PEAR::raiseError(sprintf("Kolab XML: Missing function %s!", $params['save']));
+            }
+        } elseif ($params['type'] == HORDE_KOLAB_XML_TYPE_COMPOSITE) {
+            // Possibly remove the old node first
+            if (!$append) {
+                $this->_removeNodes($parent_node, $name);
+            }
+
+            // Create a new complex node
+            $composite_node = $this->_xmldoc->create_element($name);
+            $composite_node = $parent_node->append_child($composite_node);
+            return $this->_saveArray($composite_node, $value, $params['array']);
+        } elseif ($params['type'] == HORDE_KOLAB_XML_TYPE_MULTIPLE) {
+            // Remove the old nodes first
+            $this->_removeNodes($parent_node, $name);
+
+            // Add the new nodes
+            foreach($value as $add_node) {
+                $result = $this->_saveArray($parent_node,
+                                           array($name => $add_node),
+                                           array($name => $params['array']),
+                                           true);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+            return true;
+        } else {
+            return $this->_saveDefault($parent_node, $name, $value, $params, $append);
+        }
+    }
+
+    /**
+     * Create a text node.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node  $parent   The parent of the new node.
+     * @param string          $name     The name of the child node to create.
+     * @param string          $value    The value of the child node to create.
+     *
+     * @return Horde_DOM_Node The new node.
+     */
+    function _createTextNode($parent, $name, $value)
+    {
+        $value = String::convertCharset($value, NLS::getCharset(), 'utf-8');
+
+        $node = $this->_xmldoc->create_element($name);
+
+        $node = $parent->append_child($node);
+
+        // content
+        $text = $this->_xmldoc->create_text_node($value);
+        $text = $node->append_child($text);
+
+        return $node;
+    }
+
+    /**
+     * Return the named node among a list of nodes.
+     *
+     * @access private
+     *
+     * @param array  $nodes  The list of nodes.
+     * @param string $name   The name of the node to return.
+     *
+     * @return mixed The named Horde_DOM_Node or false if no node was found.
+     */
+    function _findNode($nodes, $name)
+    {
+        foreach($nodes as $node)
+            if ($node->type == XML_ELEMENT_NODE && $node->tagname == $name) {
+                return $node;
+            }
+
+        return false;
+    }
+
+    /**
+     * Retrieve a named child from a named parent if it has the given
+     * value.
+     *
+     * @access private
+     *
+     * @param array  $nodes       The list of nodes.
+     * @param string $parent_name The name of the parent node.
+     * @param string $child_name  The name of the child node.
+     * @param string $value       The value of the child node
+     *
+     * @return mixed The specified Horde_DOM_Node or false if no node was found.
+     */
+    function _findNodeByChildData($nodes, $parent_name, $child_name, $value)
+    {
+        foreach($nodes as $node)
+        {
+            if ($node->type == XML_ELEMENT_NODE && $node->tagname == $parent_name) {
+                $children = $node->child_nodes();
+                foreach($children as $child)
+                    if ($child->type == XML_ELEMENT_NODE && $child->tagname == $child_name && $child->get_content() == $value) {
+                        return $node;
+                    }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Retrieve the content of a Horde_DOM_Node.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node  $nodes  The node that should be read.
+     *
+     * @return string|PEAR_Error The content of the node.
+     */
+    function _getNodeContent($node)
+    {
+        $value = String::convertCharset($node->get_content(), 'utf-8');
+        if (is_a($value, 'PEAR_Error')) {
+            return $value;
+        }
+        return $value;
+    }
+
+
+    /**
+     * Create a new named node on a parent node.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node $parent  The parent node.
+     * @param string         $name    The name of the new child node.
+     *
+     * @return Horde_DOM_Node The new child node.
+     */
+    function _createChildNode($parent, $name)
+    {
+        $node = $this->_xmldoc->createElement($name);
+        $node = $parent->appendChild($node);
+
+        return $node;
+    }
+
+    /**
+     * Remove named nodes from a parent node.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node $parent  The parent node.
+     * @param string         $name    The name of the children to be removed.
+     */
+    function _removeNodes($parent_node, $name)
+    {
+        while ($old_node = $this->_findNode($parent_node->child_nodes(), $name)) {
+            $parent_node->remove_child($old_node);
+        }
+    }
+
+    /**
+     * Create a new named node on a parent node if it is not already
+     * present in the given children.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node $parent    The parent node.
+     * @param array          $children  The children that might already
+     *                                  contain the node.
+     * @param string         $name      The name of the new child node.
+     *
+     * @return Horde_DOM_Node The new or already existing child node.
+     */
+    function _createOrFindChildNode($parent, $children, $name)
+    {
+        // look for existing node
+        $old_node = $this->_findNode($children, $name);
+        if ($old_node !== false) {
+            return $old_node;
+        }
+
+        // create new parent node
+        return $this->_createChildNode($parent, $name);
+    }
+
+    /**
+     * Load the different XML types.
+     *
+     * @access private
+     *
+     * @param string $node    The node to load the data from
+     * @param array  $params  Parameters for loading the value
+     *
+     * @return string|PEAR_Error The loaded value.
+     */
+    function _loadDefault($node, $params)
+    {
+        $content = $this->_getNodeContent($node);
+        if (is_a($content, 'PEAR_Error')) {
+            return $content;
+        }
+
+        switch($params['type']) {
+        case HORDE_KOLAB_XML_TYPE_DATE:
+            return Horde_Kolab_Format_Date::decodeDate($content);
+
+        case HORDE_KOLAB_XML_TYPE_DATETIME:
+            return Horde_Kolab_Format_Date::decodeDateTime($content);
+
+        case HORDE_KOLAB_XML_TYPE_DATE_OR_DATETIME:
+            return Horde_Kolab_Format_Date::decodeDateOrDateTime($content);
+
+        case HORDE_KOLAB_XML_TYPE_INTEGER:
+            return (int) $content;
+
+        case HORDE_KOLAB_XML_TYPE_BOOLEAN:
+            return (bool) $content;
+
+        default:
+            // Strings and colors are returned as they are
+            return $content;
+        }
+    }
+
+    /**
+     * Save a data array as a XML node attached to the given parent node.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node $parent_node The parent node to attach
+     *                                    the child to
+     * @param string         $name        The name of the node
+     * @param mixed          $value       The value to store
+     * @param boolean        $missing     Has the value been missing?
+     * @param boolean        $append      Should the node be appended?
+     *
+     * @return Horde_DOM_Node The new child node.
+     */
+    function _saveDefault($parent_node, $name, $value, $params, $append = false)
+    {
+        if (!$append) {
+            $this->_removeNodes($parent_node, $name);
+        }
+
+        switch ($params['type']) {
+        case HORDE_KOLAB_XML_TYPE_DATE:
+            $value = Horde_Kolab_Format_Date::encodeDate($value);
+            break;
+
+        case HORDE_KOLAB_XML_TYPE_DATETIME:
+        case HORDE_KOLAB_XML_TYPE_DATE_OR_DATETIME:
+            $value = Horde_Kolab_Format_Date::encodeDateTime($value);
+            break;
+
+        case HORDE_KOLAB_XML_TYPE_INTEGER:
+            $value = (string) $value;
+            break;
+
+        case HORDE_KOLAB_XML_TYPE_BOOLEAN:
+            if ($value) {
+                $value = 'true';
+            } else {
+                $value = 'false';
+            }
+
+            break;
+        }
+
+        // create the node
+        return $this->_createTextNode($parent_node, $name, $value);
+    }
+
+    /**
+     * Handle loading of categories. Preserve multiple categories in a hidden
+     * object field. Optionally creates categories unknown to the Horde user.
+     *
+     * @access private
+     *
+     * @param array  $object Array of strings, containing the 'categories' field.
+     */
+    function _loadMultipleCategories(&$object)
+    {
+        global $prefs;
+
+        if (empty($object['categories'])) {
+            return;
+        }
+
+        // Create horde category if needed
+        @include_once 'Horde/Prefs/CategoryManager.php';
+        if ($this->_create_categories
+            && class_exists('Prefs_CategoryManager')
+            && isset($prefs) && is_a($prefs, 'Prefs')) {
+            $cManager = new Prefs_CategoryManager();
+            $horde_categories = $cManager->get();
+        } else {
+            $cManager = null;
+            $horde_categories = null;
+        }
+
+        $kolab_categories = explode (',', $object['categories']);
+
+        $primary_category = '';
+        foreach ($kolab_categories as $kolab_category) {
+            $kolab_category = trim($kolab_category);
+
+            $valid_category = true;
+            if ($cManager && 
+                array_search($kolab_category, $horde_categories) === false) {
+                // Unknown category -> Add
+                if ($cManager->add($kolab_category) === false) {
+                    // categories might be locked
+                    $valid_category = false;
+                }
+            }
+
+            // First valid category becomes primary category
+            if ($valid_category && empty($primary_category)) {
+                $primary_category = $kolab_category;
+            }
+        }
+
+        // Backup multiple categories
+        if (count($kolab_categories) > 1) {
+            $object['_categories_all'] = $object['categories'];
+            $object['_categories_primary'] = $primary_category;
+        }
+        // Make default category visible to Horde
+        $object['categories'] = $primary_category;
+    }
+
+    /**
+     * Preserve multiple categories on save if "categories" didn't change.
+     * The name "categories" currently refers to one primary category.
+     *
+     * @access private
+     *
+     * @param array  $object Array of strings, containing the 'categories' field.
+     */
+    function _saveMultipleCategories(&$object)
+    {
+        // Check for multiple categories.
+        if (!isset($object['_categories_all']) || !isset($object['_categories_primary'])
+            || !isset($object['categories']))
+        {
+            return;
+        }
+
+        // Preserve multiple categories if "categories" didn't change
+        if ($object['_categories_primary'] == $object['categories']) {
+            $object['categories'] = $object['_categories_all'];
+        }
+    }
+
+    /**
+     * Load the object creation date.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node  $node    The original node if set.
+     * @param boolean         $missing Has the node been missing?
+     *
+     * @return string|PEAR_Error The creation date.
+     */
+    function _loadCreationDate($node, $missing)
+    {
+        if ($missing) {
+            // Be gentle and accept a missing creation date.
+            return time();
+        }
+        return $this->_loadDefault($node,
+                                   array('type' => HORDE_KOLAB_XML_TYPE_DATETIME));
+    }
+
+    /**
+     * Save the object creation date.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node $parent_node The parent node to attach the child
+     *                                    to.
+     * @param string         $name        The name of the node.
+     * @param mixed          $value       The value to store.
+     * @param boolean        $missing     Has the value been missing?
+     *
+     * @return Horde_DOM_Node The new child node.
+     */
+    function _saveCreationDate($parent_node, $name, $value, $missing)
+    {
+        // Only create the creation date if it has not been set before
+        if ($missing) {
+            $value = time();
+        }
+        return $this->_saveDefault($parent_node,
+                                   $name,
+                                   $value,
+                                   array('type' => HORDE_KOLAB_XML_TYPE_DATETIME));
+    }
+
+    /**
+     * Load the object modification date.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node  $node    The original node if set.
+     * @param boolean         $missing Has the node been missing?
+     *
+     * @return string The last modification date.
+     */
+    function _loadModificationDate($node, $missing)
+    {
+        if ($missing) {
+            // Be gentle and accept a missing modification date.
+            return time();
+        }
+        return $this->_loadDefault($node,
+                                   array('type' => HORDE_KOLAB_XML_TYPE_DATETIME));
+    }
+
+    /**
+     * Save the object modification date.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node $parent_node The parent node to attach
+     *                                    the child to.
+     * @param string         $name        The name of the node.
+     * @param mixed          $value       The value to store.
+     * @param boolean        $missing     Has the value been missing?
+     *
+     * @return Horde_DOM_Node The new child node.
+     */
+    function _saveModificationDate($parent_node, $name, $value, $missing)
+    {
+        // Always store now as modification date
+        return $this->_saveDefault($parent_node,
+                                   $name,
+                                   time(),
+                                   array('type' => HORDE_KOLAB_XML_TYPE_DATETIME));
+    }
+
+    /**
+     * Load the name of the last client that modified this object
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node  $node    The original node if set.
+     * @param boolean         $missing Has the node been missing?
+     *
+     * @return string The last modification date.
+     */
+    function _loadProductId($node, $missing)
+    {
+        if ($missing) {
+            // Be gentle and accept a missing product id
+            return '';
+        }
+        return $this->_getNodeContent($node);
+    }
+
+    /**
+     * Save the name of the last client that modified this object.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node $parent_node The parent node to attach
+     *                                    the child to.
+     * @param string         $name        The name of the node.
+     * @param mixed          $value       The value to store.
+     * @param boolean        $missing     Has the value been missing?
+     *
+     * @return Horde_DOM_Node The new child node.
+     */
+    function _saveProductId($parent_node, $name, $value, $missing)
+    {
+        // Always store now as modification date
+        return $this->_saveDefault($parent_node,
+                                   $name,
+                                   HORDE_KOLAB_XML_PRODUCT_ID,
+                                   array('type' => HORDE_KOLAB_XML_TYPE_STRING));
+    }
+
+    /**
+     * Load recurrence information.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node  $node    The original node if set.
+     * @param boolean         $missing Has the node been missing?
+     *
+     * @return array|PEAR_Error The recurrence information.
+     */
+    function _loadRecurrence($node, $missing)
+    {
+        if ($missing) {
+            return null;
+        }
+
+        // Collect all child nodes
+        $children = $node->child_nodes();
+
+        $recurrence = $this->_loadArray($children, $this->_fields_recurrence);
+
+        // Get the cycle type (must be present)
+        $recurrence['cycle'] = $node->get_attribute('cycle');
+        // Get the sub type (may be present)
+        $recurrence['type']  = $node->get_attribute('type');
+
+        // Exclusions.
+        if (isset($recurrence['exclusion'])) {
+            $exceptions = array();
+            foreach($recurrence['exclusion'] as $exclusion) {
+                if (!empty($exclusion)) {
+                    list($year, $month, $mday) = sscanf($exclusion, '%04d-%02d-%02d');
+                    $exceptions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
+                }
+            }
+            $recurrence['exceptions'] = $exceptions;
+        }
+
+        // Completed dates.
+        if (isset($recurrence['complete'])) {
+            $completions = array();
+            foreach($recurrence['complete'] as $complete) {
+                if (!empty($complete)) {
+                    list($year, $month, $mday) = sscanf($complete, '%04d-%02d-%02d');
+                    $completions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
+                }
+            }
+            $recurrence['completions'] = $completions;
+        }
+
+        // Range is special
+        foreach($children as $child) {
+            if ($child->tagname == "range") {
+                $recurrence['range-type'] = $child->get_attribute('type');
+            }
+        }
+
+        if (isset($recurrence['range']) && isset($recurrence['range-type'])
+            && $recurrence['range-type'] == 'date') {
+            $recurrence['range'] = Horde_Kolab_Format_Date::decodeDate($recurrence['range']);
+        }
+
+        // Sanity check
+        $valid = $this->_validateRecurrence($recurrence);
+        if (is_a($valid, 'PEAR_Error')) {
+            return $valid;
+        }
+
+        return $recurrence;
+    }
+
+    /**
+     * Validate recurrence hash information.
+     *
+     * @access private
+     *
+     * @param array  $recurrence  Recurrence hash loaded from XML.
+     *
+     * @return boolean|PEAR_Error True on success.
+     */
+    function _validateRecurrence(&$recurrence)
+    {
+        if (!isset($recurrence['cycle'])) {
+              return PEAR::raiseError('recurrence tag error: cycle attribute missing');
+        }
+
+        if (!isset($recurrence['interval'])) {
+              return PEAR::raiseError('recurrence tag error: interval tag missing');
+        }
+        $interval = $recurrence['interval'];
+        if ($interval < 0) {
+            return PEAR::raiseError('recurrence tag error: interval cannot be below zero: ' . $interval);
+        }
+
+        if ($recurrence['cycle'] == 'weekly') {
+            // Check for <day>
+            if (!isset($recurrence['day']) || count($recurrence['day']) == 0) {
+                return PEAR::raiseError('recurrence tag error: day tag missing for weekly recurrence');
+            }
+        }
+
+        // The code below is only for monthly or yearly recurrences
+        if ($recurrence['cycle'] != 'monthly' && $recurrence['cycle'] != 'yearly')
+            return true;
+
+        if (!isset($recurrence['type'])) {
+            return PEAR::raiseError('recurrence tag error: type attribute missing');
+        }
+
+        if (!isset($recurrence['daynumber'])) {
+            return PEAR::raiseError('recurrence tag error: daynumber tag missing');
+        }
+        $daynumber = $recurrence['daynumber'];
+        if ($daynumber < 0) {
+            return PEAR::raiseError('recurrence tag error: daynumber cannot be below zero: ' . $daynumber);
+        }
+
+        if ($recurrence['type'] == 'daynumber') {
+            if ($recurrence['cycle'] == 'yearly' && $daynumber > 366) {
+                return PEAR::raiseError('recurrence tag error: daynumber cannot be larger than 366 for yearly recurrences: ' . $daynumber);
+            } else if ($recurrence['cycle'] == 'monthly' && $daynumber > 31) {
+                return PEAR::raiseError('recurrence tag error: daynumber cannot be larger than 31 for monthly recurrences: ' . $daynumber);
+            }
+        } else if ($recurrence['type'] == 'weekday') {
+            // daynumber is the week of the month
+            if ($daynumber > 5) {
+                return PEAR::raiseError('recurrence tag error: daynumber cannot be larger than 5 for type weekday: ' . $daynumber);
+            }
+
+            // Check for <day>
+            if (!isset($recurrence['day']) || count($recurrence['day']) == 0) {
+                return PEAR::raiseError('recurrence tag error: day tag missing for type weekday');
+            }
+        }
+
+        if (($recurrence['type'] == 'monthday' || $recurrence['type'] == 'yearday')
+            && $recurrence['cycle'] == 'monthly')
+        {
+            return PEAR::raiseError('recurrence tag error: type monthday/yearday is only allowed for yearly recurrences');
+        }
+
+        if ($recurrence['cycle'] == 'yearly') {
+            if ($recurrence['type'] == 'monthday') {
+                // daynumber and month
+                if (!isset($recurrence['month'])) {
+                    return PEAR::raiseError('recurrence tag error: month tag missing for type monthday');
+                }
+                if ($daynumber > 31) {
+                    return PEAR::raiseError('recurrence tag error: daynumber cannot be larger than 31 for type monthday: ' . $daynumber);
+                }
+            } else if ($recurrence['type'] == 'yearday') {
+                if ($daynumber > 366) {
+                    return PEAR::raiseError('recurrence tag error: daynumber cannot be larger than 366 for type yearday: ' . $daynumber);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Save recurrence information.
+     *
+     * @access private
+     *
+     * @param Horde_DOM_Node $parent_node The parent node to attach
+     *                                    the child to.
+     * @param string         $name        The name of the node.
+     * @param mixed          $value       The value to store.
+     * @param boolean        $missing     Has the value been missing?
+     *
+     * @return Horde_DOM_Node The new child node.
+     */
+    function _saveRecurrence($parent_node, $name, $value, $missing)
+    {
+        $this->_removeNodes($parent_node, $name);
+
+        if (empty($value)) {
+            return false;
+        }
+
+        // Exclusions.
+        if (isset($value['exceptions'])) {
+            $exclusions = array();
+            foreach($value['exceptions'] as $exclusion) {
+                if (!empty($exclusion)) {
+                    list($year, $month, $mday) = sscanf($exclusion, '%04d%02d%02d');
+                    $exclusions[] = "$year-$month-$mday";
+                }
+            }
+            $value['exclusion'] = $exclusions;
+        }
+
+        // Completed dates.
+        if (isset($value['completions'])) {
+            $completions = array();
+            foreach($value['completions'] as $complete) {
+                if (!empty($complete)) {
+                    list($year, $month, $mday) = sscanf($complete, '%04d%02d%02d');
+                    $completions[] = "$year-$month-$mday";
+                }
+            }
+            $value['complete'] = $completions;
+        }
+
+        if (isset($value['range'])
+            && isset($value['range-type']) && $value['range-type'] == 'date') {
+            $value['range'] = Horde_Kolab_Format_Date::encodeDate($value['range']);
+        }
+
+        $r_node = $this->_xmldoc->create_element($name);
+        $r_node = $parent_node->append_child($r_node);
+
+        // Save normal fields
+        $result = $this->_saveArray($r_node, $value, $this->_fields_recurrence);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        // Add attributes
+        $r_node->set_attribute("cycle", $value['cycle']);
+        if (isset($value['type'])) {
+            $r_node->set_attribute("type", $value['type']);
+        }
+
+        $child = $this->_findNode($r_node->child_nodes(), "range");
+        if ($child) {
+            $child->set_attribute("type", $value['range-type']);
+        }
+
+        return $r_node;
+    }
+}
diff --git a/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/annotation.php b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/annotation.php
new file mode 100644 (file)
index 0000000..77c733d
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+/**
+ * Implementation for IMAP folder annotations in the Kolab XML format.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/annotation.php,v 1.5 2009/01/06 17:49:23 jan Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ * Kolab XML handler for IMAP folder annotations.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/annotation.php,v 1.5 2009/01/06 17:49:23 jan Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @since   Horde 3.2
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_XML_annotation extends Horde_Kolab_Format_XML {
+    /**
+     * Specific data fields for the prefs object
+     *
+     * @var Kolab
+     */
+    var $_fields_specific;
+
+    /**
+     * Constructor
+     */
+    function Horde_Kolab_Format_XML_annotation()
+    {
+        $this->_root_name = 'annotations';
+
+        /** Specific preferences fields, in kolab format specification order
+         */
+        $this->_fields_specific = array(
+            'annotation' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                'array'   => array(
+                    'type' => HORDE_KOLAB_XML_TYPE_STRING,
+                    'value' => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                ),
+            ),
+        );
+
+        parent::Horde_Kolab_Format_XML();
+    }
+
+    /**
+     * Load the groupware object based on the specifc XML values.
+     *
+     * @access protected
+     *
+     * @param array $children An array of XML nodes.
+     *
+     * @return array|PEAR_Error Array with the object data
+     */
+    function _load(&$children)
+    {
+        $object = $this->_loadArray($children, $this->_fields_specific);
+        if (is_a($object, 'PEAR_Error')) {
+            return $object;
+        }
+
+        $result = array();
+        foreach ($object['annotation'] as $annotation) {
+            list($key, $value) = split('#', $annotation, 2);
+            $result[base64_decode($key)] = base64_decode($value);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Save the specific XML values.
+     *
+     * @access protected
+     *
+     * @param array $root     The XML document root.
+     * @param array $object   The resulting data array.
+     *
+     * @return boolean|PEAR_Error True on success.
+     */
+    function _save($root, $object)
+    {
+        $annotations = array();
+        foreach ($object as $key => $value) {
+            if ($key != 'uid') {
+                $annotations['annotation'][] = base64_encode($key) . '#' . base64_encode($value);
+            }
+        }
+
+        return $this->_saveArray($root, $annotations, $this->_fields_specific);
+    }
+}
diff --git a/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/contact.php b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/contact.php
new file mode 100644 (file)
index 0000000..ffae144
--- /dev/null
@@ -0,0 +1,519 @@
+<?php
+/**
+ * Implementation for contacts in the Kolab XML format.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/contact.php,v 1.7 2008/12/12 11:25:52 wrobel Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ * Kolab XML handler for contact groupware objects
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/contact.php,v 1.7 2008/12/12 11:25:52 wrobel Exp $
+ *
+ * Copyright 2007-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @since   Horde 3.2
+ * @author  Thomas Jarosch <thomas.jarosch@intra2net.com>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_XML_contact extends Horde_Kolab_Format_XML {
+
+    /**
+     * Specific data fields for the contact object
+     *
+     * @var array
+     */
+    var $_fields_specific;
+
+    /**
+     * Structure of the name field
+     *
+     * @var array
+     */
+    var $_fields_name = array(
+        'given-name' => array (
+            'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        ),
+        'middle-names' => array (
+            'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        ),
+        'last-name' => array (
+            'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        ),
+        'full-name' => array (
+            'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        ),
+        'initials' => array (
+            'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        ),
+        'prefix' => array (
+            'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        ),
+        'suffix' => array (
+            'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+            'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        )
+    );
+
+    /**
+     * Structure of an address field
+     *
+     * @var array
+     */
+    var $_fields_address = array(
+        'type'    => HORDE_KOLAB_XML_TYPE_COMPOSITE,
+        'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        'array'   => array(
+            'type' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => 'home',
+            ),
+            'street' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'locality' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'region' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'postal-code' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'country' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+        )
+    );
+
+    /**
+     * Structure of a phone field
+     *
+     * @var array
+     */
+    var $_fields_phone = array(
+        'type'    => HORDE_KOLAB_XML_TYPE_COMPOSITE,
+        'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+        'array'   => array(
+            'type' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => '',
+            ),
+            'number' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+        ),
+    );
+
+    /**
+     * Address types
+     *
+     * @var array
+     */
+    var $_address_types = array(
+        'business',
+        'home',
+        'other',
+    );
+
+    /**
+     * Phone types
+     *
+     * @var array
+     */
+    var $_phone_types = array(
+        'business1',
+        'business2',
+        'businessfax',
+        'callback',
+        'car',
+        'company',
+        'home1',
+        'home2',
+        'homefax',
+        'isdn',
+        'mobile',
+        'pager',
+        'primary',
+        'radio',
+        'telex',
+        'ttytdd',
+        'assistant',
+        'other',
+    );
+
+    /**
+     * Constructor
+     */
+    function Horde_Kolab_Format_XML_contact()
+    {
+        $this->_root_name = "contact";
+
+        /** Specific task fields, in kolab format specification order
+         */
+        $this->_fields_specific = array(
+            'name' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_COMPOSITE,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                'array'   => $this->_fields_name,
+            ),
+            'free-busy-url' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'organization' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'web-page' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'im-address' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'department' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'office-location' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'profession' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'job-title' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'manager-name' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'assistant' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'nick-name' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'spouse-name' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'birthday' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'anniversary' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'picture' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'children' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'gender' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'language' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'address' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                'array'   => $this->_fields_address,
+            ),
+            'email' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                'array'   => $this->_fields_simple_person,
+            ),
+            'phone' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                'array'   => $this->_fields_phone,
+            ),
+            'preferred-address' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'latitude' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'longitude' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            // Horde specific fields
+            'pgp-publickey' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            // Support for broken clients
+            'website' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'im-adress' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+        );
+
+        parent::Horde_Kolab_Format_XML();
+    }
+
+    /**
+     * Load the groupware object based on the specifc XML values.
+     *
+     * @access protected
+     *
+     * @param array $children An array of XML nodes.
+     *
+     * @return array|PEAR_Error Array with the object data.
+     */
+    function _load(&$children)
+    {
+        $object = $this->_loadArray($children, $this->_fields_specific);
+        if (is_a($object, 'PEAR_Error')) {
+            return $object;
+        }
+
+        // Handle name fields
+        if (isset($object['name'])) {
+            $object = array_merge($object['name'], $object);
+            unset($object['name']);
+        }
+
+        // Handle email fields
+        $emails = array();
+        if (isset($object['email'])) {
+            foreach ($object['email'] as $email) {
+                $smtp_address = $email['smtp-address'];
+                if (!empty($smtp_address)) {
+                    $emails[] = $smtp_address;
+                }
+            }
+        }
+        $object['emails'] = implode(', ', $emails);
+
+        // Handle phone fields
+        if (isset($object['phone'])) {
+            foreach ($object['phone'] as $phone) {
+                if (isset($phone['number']) &&
+                    in_array($phone['type'], $this->_phone_types)) {
+                    $object["phone-" . $phone['type']] = $phone['number'];
+                }
+            }
+        }
+
+        // Handle address fields
+        if (isset($object['address'])) {
+            foreach ($object['address'] as $address) {
+                if (in_array($address['type'], $this->_address_types)) {
+                    foreach ($address as $name => $value) {
+                        $object["addr-" . $address['type'] . "-" . $name] = $value;
+                    }
+                }
+            }
+        }
+
+        // Handle gender field
+        if (isset($object['gender'])) {
+            $gender = $object['gender'];
+
+            if ($gender == "female") {
+                $object['gender'] = 1;
+            } else if ($gender == "male") {
+                $object['gender'] = 0;
+            } else {
+                // unspecified gender
+                unset($object['gender']);
+            }
+        }
+
+        // Compatibility with broken clients
+        $broken_fields = array("website" => "web-page",
+                               "im-adress" => "im-address");
+        foreach ($broken_fields as $broken_field => $real_field) {
+            if (!empty($object[$broken_field]) && empty($object[$real_field])) {
+                $object[$real_field] = $object[$broken_field];
+            }
+            unset($object[$broken_field]);
+        }
+
+        $object['__type'] = 'Object';
+
+        return $object;
+    }
+
+    /**
+     * Save the  specifc XML values.
+     *
+     * @access protected
+     *
+     * @param array $root     The XML document root.
+     * @param array $object   The resulting data array.
+     *
+     * @return boolean|PEAR_Error True on success.
+     */
+    function _save($root, $object)
+    {
+        // Handle name fields
+        $name = array();
+        foreach (array_keys($this->_fields_name) as $key) {
+            if (isset($object[$key])) {
+                $name[$key] = $object[$key];
+                unset($object[$key]);
+            }
+        }
+        $object['name'] = $name;
+
+        // Handle email fields
+        if (!isset($object['emails'])) {
+            $emails = array();
+        } else {
+            $emails = explode(',', $object['emails']);
+        }
+
+        if (isset($object['email']) && 
+            !in_array($object['email'], $emails)) {
+            $emails[] = $object['email'];
+        }
+
+        $object['email'] = array();
+
+        foreach ($emails as $email) {
+            $email = trim($email);
+            if (!empty($email)) {
+                $new_email = array('display-name' => $object['name']['full-name'],
+                                   'smtp-address' => $email);
+                $object['email'][] = $new_email;
+            }
+        }
+
+        // Handle phone fields
+        if (!isset($object['phone'])) {
+            $object['phone'] = array();
+        }
+        foreach ($this->_phone_types as $type) {
+            $key = 'phone-' . $type;
+            if (array_key_exists($key, $object)) {
+                $new_phone = array('type'   => $type,
+                                   'number' => $object[$key]);
+
+                // Update existing phone entry of this type
+                $updated = false;
+                foreach($object['phone'] as $index => $phone) {
+                    if ($phone['type'] == $type) {
+                        $object['phone'][$index] = $new_phone;
+                        $updated = true;
+                        break;
+                    }
+                }
+                if (!$updated) {
+                    $object['phone'][] = $new_phone;
+                }
+            }
+        }
+
+        // Phone cleanup: remove empty numbers
+        foreach($object['phone'] as $index => $phone) {
+            if (empty($phone['number'])) {
+                unset($object['phone'][$index]);
+            }
+        }
+
+        // Handle address fields
+        if (!isset($object['address'])) {
+            $object['address'] = array();
+        }
+
+        foreach ($this->_address_types as $type) {
+            $basekey = 'addr-' . $type . '-';
+            $new_address = array('type'   => $type);
+            foreach (array_keys($this->_fields_address['array']) as $subkey) {
+                $key = $basekey . $subkey;
+                if (array_key_exists($key, $object)) {
+                    $new_address[$subkey] = $object[$key];
+                }
+            }
+
+            // Update existing address entry of this type
+            $updated = false;
+            foreach($object['address'] as $index => $address) {
+                if ($address['type'] == $type) {
+                    $object['address'][$index] = $new_address;
+                    $updated = true;
+                }
+            }
+            if (!$updated) {
+                $object['address'][] = $new_address;
+            }
+        }
+
+        // Address cleanup: remove empty addresses
+        foreach($object['address'] as $index => $address) {
+            $all_empty = true;
+            foreach($address as $name => $value) {
+                if (!empty($value) && $name != "type") {
+                    $all_empty = false;
+                    break;
+                }
+            }
+
+            if ($all_empty) {
+                unset($object['address'][$index]);
+            }
+        }
+
+        // Handle gender field
+        if (isset($object['gender'])) {
+            $gender = $object['gender'];
+
+            if ($gender == "0") {
+                $object['gender'] = "male";
+            } else if ($gender == "1") {
+                $object['gender'] = "female";
+            } else {
+                // unspecified gender
+                unset($object['gender']);
+            }
+        }
+
+        // Do the actual saving
+        return $this->_saveArray($root, $object, $this->_fields_specific);
+    }
+}
diff --git a/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/distributionlist.php b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/distributionlist.php
new file mode 100644 (file)
index 0000000..b81573f
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Implementation for distributionlists in the Kolab XML format.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/distributionlist.php,v 1.6 2008/12/12 11:25:52 wrobel Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ * Kolab XML handler for distributionlist groupware objects
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/distributionlist.php,v 1.6 2008/12/12 11:25:52 wrobel Exp $
+ *
+ * Copyright 2007-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @since   Horde 3.2
+ * @author  Thomas Jarosch <thomas.jarosch@intra2net.com>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @author  Mike Gabriel <m.gabriel@das-netzwerkteam.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_XML_distributionlist extends Horde_Kolab_Format_XML {
+
+    /**
+     * Specific data fields for the contact object
+     *
+     * @var array
+     */
+    var $_fields_specific;
+
+    /**
+     * Constructor
+     */
+    function Horde_Kolab_Format_XML_distributionlist()
+    {
+        $this->_root_name = "distribution-list";
+
+        /** Specific task fields, in kolab format specification order
+         */
+        $this->_fields_specific = array(
+                'display-name' => array(
+                    'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                    'value'   => HORDE_KOLAB_XML_VALUE_NOT_EMPTY
+                ),
+                'member' => array(
+                    'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+                    'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                    'array'   => $this->_fields_simple_person,
+                )
+            );
+
+        parent::Horde_Kolab_Format_XML();
+    }
+
+    /**
+     * Load the groupware object based on the specifc XML values.
+     *
+     * @access protected
+     *
+     * @param array $children An array of XML nodes.
+     *
+     * @return array|PEAR_Error Array with data.
+     */
+    function _load(&$children)
+    {
+        $object = $this->_loadArray($children, $this->_fields_specific);
+        if (is_a($object, 'PEAR_Error')) {
+            return $object;
+        }
+
+        // Map the display-name of a kolab dist list to horde's lastname attribute
+        if (isset($object['display-name'])) {
+            $object['last-name'] = $object['display-name'];
+            unset($object['display-name']);
+        }
+
+        // the mapping from $object['member'] as stored in XML back to Turba_Objects (contacts)
+        // must be performed in the Kolab_IMAP storage driver as we need access to the search
+        // facilities of the kolab storage driver.
+
+        $object['__type'] = 'Group';
+        return $object;
+    }
+
+    /**
+     * Save the  specifc XML values.
+     *
+     * @access protected
+     *
+     * @param array $root     The XML document root.
+     * @param array $object   The resulting data array.
+     *
+     * @return boolean|PEAR_Error True on success.
+     */
+    function _save($root, $object)
+    {
+        // Map the display-name of a kolab dist list to horde's lastname attribute
+        if (isset($object['last-name'])) {
+            $object['display-name'] = $object['last-name'];
+            unset($object['last-name']);
+        }
+
+        return $this->_saveArray($root, $object, $this->_fields_specific);
+    }
+}
diff --git a/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/event.php b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/event.php
new file mode 100644 (file)
index 0000000..24fc8b9
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+/**
+ * Implementation for events in the Kolab XML format.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/event.php,v 1.4 2008/12/12 11:25:52 wrobel Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/** Kolab date handling functions. */
+require_once 'Horde/Kolab/Format/Date.php';
+
+/**
+ * Kolab XML handler for event groupware objects.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/event.php,v 1.4 2008/12/12 11:25:52 wrobel Exp $
+ *
+ * Copyright 2007-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @since   Horde 3.2
+ * @author  Thomas Jarosch <thomas.jarosch@intra2net.com>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_XML_event extends Horde_Kolab_Format_XML {
+    /**
+     * Specific data fields for the contact object
+     *
+     * @var array
+     */
+    var $_fields_specific;
+
+    /**
+     * Constructor
+     */
+    function Horde_Kolab_Format_XML_event()
+    {
+        $this->_root_name = 'event';
+
+        /** Specific event fields, in kolab format specification order
+         */
+        $this->_fields_specific = array(
+            'summary' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'location' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'organizer' => $this->_fields_simple_person,
+            'start-date' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
+            ),
+            'alarm' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_INTEGER,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'recurrence' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_COMPOSITE,
+                'value'   => HORDE_KOLAB_XML_VALUE_CALCULATED,
+                'load'    => 'Recurrence',
+                'save'    => 'Recurrence',
+            ),
+            'attendee' => $this->_fields_attendee,
+            'show-time-as' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'color-label' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'end-date' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
+            ),
+        );
+
+        parent::Horde_Kolab_Format_XML();
+    }
+
+    /**
+     * Load event XML values and translate start/end date.
+     *
+     * @access protected
+     *
+     * @param array $children An array of XML nodes.
+     *
+     * @return array|PEAR_Error Array with the object data.
+     */
+    function _load(&$children)
+    {
+        $object = parent::_load($children);
+        if (is_a($object, 'PEAR_Error')) {
+            return $object;
+        }
+
+        // Translate start/end date including full day events
+        if (strlen($object['start-date']) == 10) {
+            $object['start-date'] = Horde_Kolab_Format_Date::decodeDate($object['start-date']);
+            $object['end-date'] = Horde_Kolab_Format_Date::decodeDate($object['end-date']) + 24*60*60;
+        } else {
+            $object['start-date'] = Horde_Kolab_Format_Date::decodeDateTime($object['start-date']);
+            $object['end-date'] = Horde_Kolab_Format_Date::decodeDateTime($object['end-date']);
+        }
+
+        return $object;
+    }
+
+    /**
+     * Save event XML values and translate start/end date.
+     *
+     * @access protected
+     *
+     * @param array $root     The XML document root.
+     * @param array $object   The resulting data array.
+     *
+     * @return boolean|PEAR_Error True on success.
+     */
+    function _save($root, $object)
+    {
+        // Translate start/end date including full day events
+        if (!empty($object['_is_all_day'])) {
+            $object['start-date'] = Horde_Kolab_Format_Date::encodeDate($object['start-date']);
+            $object['end-date'] = Horde_Kolab_Format_Date::encodeDate($object['end-date'] - 24*60*60);
+        } else {
+            $object['start-date'] = Horde_Kolab_Format_Date::encodeDateTime($object['start-date']);
+            $object['end-date'] = Horde_Kolab_Format_Date::encodeDateTime($object['end-date']);
+        }
+
+        return parent::_save($root, $object);
+    }
+}
diff --git a/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/hprefs.php b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/hprefs.php
new file mode 100644 (file)
index 0000000..58b0137
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Implementation for horde user preferences in the Kolab XML format.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/hprefs.php,v 1.8 2009/01/06 17:49:23 jan Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ * Kolab XML handler for client preferences.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/hprefs.php,v 1.8 2009/01/06 17:49:23 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @since   Horde 3.2
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_XML_hprefs extends Horde_Kolab_Format_XML {
+    /**
+     * Specific data fields for the prefs object
+     *
+     * @var Kolab
+     */
+    var $_fields_specific;
+
+    /**
+     * Automatically create categories if they are missing?
+     *
+     * @var boolean
+     */
+    var $_create_categories = false;
+
+    /**
+     * Constructor
+     */
+    function Horde_Kolab_Format_XML_hprefs()
+    {
+        $this->_root_name = 'h-prefs';
+
+        /** Specific preferences fields, in kolab format specification order
+         */
+        $this->_fields_specific = array(
+            'application' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'pref' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                'array'   => array(
+                    'type' => HORDE_KOLAB_XML_TYPE_STRING,
+                    'value' => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                ),
+            ),
+        );
+
+        parent::Horde_Kolab_Format_XML();
+    }
+
+    /**
+     * Load an object based on the given XML string.
+     *
+     * @param string $xmltext  The XML of the message as string.
+     *
+     * @return array|PEAR_Error The data array representing the object.
+     */
+    function load(&$xmltext)
+    {
+        $object = parent::load($xmltext);
+
+        if (empty($object['application'])) {
+            if (!empty($object['categories'])) {
+                $object['application'] = $object['categories'];
+                unset($object['categories']);
+            } else {
+                return PEAR::raiseError('Preferences XML object is missing an application setting.');
+            }
+        }
+
+        return $object;
+    }
+
+    /**
+     * Convert the data to a XML string.
+     *
+     * @param array $attributes  The data array representing the note.
+     *
+     * @return string|PEAR_Error The data as XML string.
+     */
+    function save($object)
+    {
+        if (empty($object['application'])) {
+            if (!empty($object['categories'])) {
+                $object['application'] = $object['categories'];
+                unset($object['categories']);
+            } else {
+                return PEAR::raiseError('Preferences XML object is missing an application setting.');
+            }
+        }
+
+        return parent::save($object);
+    }
+}
diff --git a/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/note.php b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/note.php
new file mode 100644 (file)
index 0000000..bb69c2e
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+/**
+ * Implementation for notes in the Kolab XML format.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/note.php,v 1.6 2008/12/12 11:25:52 wrobel Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ * Kolab XML handler for note groupware objects.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/note.php,v 1.6 2008/12/12 11:25:52 wrobel Exp $
+ *
+ * Copyright 2007-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @since   Horde 3.2
+ * @author  Thomas Jarosch <thomas.jarosch@intra2net.com>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_XML_note extends Horde_Kolab_Format_XML {
+    /**
+     * Specific data fields for the note object
+     *
+     * @var Kolab
+     */
+    var $_fields_specific;
+
+    /**
+     * Constructor
+     */
+    function Horde_Kolab_Format_XML_note()
+    {
+        $this->_root_name = 'note';
+
+        /** Specific note fields, in kolab format specification order
+         */
+        $this->_fields_specific = array(
+            'summary' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => '',
+            ),
+            'background-color' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_COLOR,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => '#000000',
+            ),
+            'foreground-color' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_COLOR,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => '#ffff00',
+            ),
+        );
+
+        parent::Horde_Kolab_Format_XML();
+    }
+
+    /**
+     * Load the groupware object based on the specifc XML values.
+     *
+     * @access protected
+     *
+     * @param array $children An array of XML nodes.
+     *
+     * @return array|PEAR_Error Array with the object data
+     */
+    function _load(&$children)
+    {
+        $object = $this->_loadArray($children, $this->_fields_specific);
+        if (is_a($object, 'PEAR_Error')) {
+            return $object;
+        }
+
+        $object['desc'] = $object['summary'];
+        unset($object['summary']);
+
+        return $object;
+    }
+
+    /**
+     * Save the specific XML values.
+     *
+     * @access protected
+     *
+     * @param array $root     The XML document root.
+     * @param array $object   The resulting data array.
+     *
+     * @return boolean|PEAR_Error True on success.
+     */
+    function _save($root, $object)
+    {
+        $object['summary'] = $object['desc'];
+        unset($object['desc']);
+
+        return $this->_saveArray($root, $object, $this->_fields_specific);
+    }
+}
diff --git a/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/task.php b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/task.php
new file mode 100644 (file)
index 0000000..50d516f
--- /dev/null
@@ -0,0 +1,191 @@
+<?php
+/**
+ * Implementation for tasks in the Kolab XML format.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/task.php,v 1.7 2008/12/12 11:25:52 wrobel Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ * Kolab XML handler for task groupware objects.
+ *
+ * $Horde: framework/Kolab_Format/lib/Horde/Kolab/Format/XML/task.php,v 1.7 2008/12/12 11:25:52 wrobel Exp $
+ *
+ * Copyright 2007-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @since   Horde 3.2
+ * @author  Thomas Jarosch <thomas.jarosch@intra2net.com>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_XML_task extends Horde_Kolab_Format_XML {
+    /**
+     * Specific data fields for the note object
+     *
+     * @var array
+     */
+    var $_fields_specific;
+
+    /**
+     * Constructor
+     */
+    function Horde_Kolab_Format_XML_task()
+    {
+        $this->_root_name = 'task';
+
+        /** Specific task fields, in kolab format specification order
+         */
+        $this->_fields_specific = array(
+            'summary' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => '',
+            ),
+            'location' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => '',
+            ),
+            'creator'   => $this->_fields_simple_person,
+            'organizer' => $this->_fields_simple_person,
+            'start-date' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_DATE_OR_DATETIME,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'alarm' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_INTEGER,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'recurrence' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_COMPOSITE,
+                'value'   => HORDE_KOLAB_XML_VALUE_CALCULATED,
+                'load'    => 'Recurrence',
+                'save'    => 'Recurrence',
+            ),
+            'attendee' => $this->_fields_attendee,
+            'priority' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_INTEGER,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => 3,
+            ),
+            'completed' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_INTEGER,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => 0,
+            ),
+            'status' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                'default' => 'not-started',
+            ),
+            'due-date' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_DATE_OR_DATETIME,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'parent' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            // These are not part of the Kolab specification but it is
+            // ok if the client supports additional entries
+            'estimate' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'completed_date' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_DATE_OR_DATETIME,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+        );
+
+        parent::Horde_Kolab_Format_XML();
+    }
+
+    /**
+     * Load the groupware object based on the specifc XML values.
+     *
+     * @param array $children An array of XML nodes.
+     *
+     * @return array|PEAR_Error Array with data.
+     */
+    function _load(&$children)
+    {
+        $object = $this->_loadArray($children, $this->_fields_specific);
+        if (is_a($object, 'PEAR_Error')) {
+            return $object;
+        }
+
+        $object['name'] = $object['summary'];
+        unset($object['summary']);
+
+        if (empty($object['completed-date'])) {
+            $object['completed-date'] = null;
+        }
+
+        if (empty($object['alarm'])) {
+            $object['alarm'] = null;
+        }
+
+        if (isset($object['due-date'])) {
+            $object['due'] = $object['due-date'];
+            unset($object['due-date']);
+        } else {
+            $object['due'] = null;
+        }
+
+        if (isset($object['start-date'])) {
+            $object['start'] = $object['start-date'];
+            unset($object['start-date']);
+        } else {
+            $object['start'] = null;
+        }
+
+        if (!isset($object['estimate'])) {
+            $object['estimate'] = null;
+        } else {
+            $object['estimate'] = (float) $object['estimate'];
+        }
+
+        if (!isset($object['parent'])) {
+            $object['parent'] = null;
+        }
+
+        $object['completed'] = (bool) Kolab::percentageToBoolean($object['completed']);
+
+        if (isset($object['organizer']) && isset($object['organizer']['smtp-address'])) {
+            $object['assignee'] = $object['organizer']['smtp-address'];
+        }
+
+        return $object;
+    }
+
+    /**
+     * Save the specific XML values.
+     *
+     * @param array $root     The XML document root.
+     * @param array $object   The resulting data array.
+     *
+     * @return boolean|PEAR_Error True on success.
+     */
+    function _save($root, $object)
+    {
+        $object['summary'] = $object['name'];
+        unset($object['name']);
+
+        $object['due-date'] = $object['due'];
+        unset($object['due']);
+
+        $object['start-date'] = $object['start'];
+        unset($object['start']);
+
+        $object['estimate'] = number_format($object['estimate'], 2);
+
+        $object['completed'] = Kolab::BooleanToPercentage($object['completed']);
+
+        return $this->_saveArray($root, $object, $this->_fields_specific);
+    }
+}
diff --git a/framework/Kolab_Format/package.xml b/framework/Kolab_Format/package.xml
new file mode 100644 (file)
index 0000000..abd00ce
--- /dev/null
@@ -0,0 +1,286 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Kolab_Format</name>
+ <channel>pear.horde.org</channel>
+ <summary>A package for reading/writing Kolab data formats</summary>
+ <description>This package allows to convert Kolab data objects from
+ XML to hashes.</description>
+ <lead>
+  <name>Gunnar Wrobel</name>
+  <user>wrobel</user>
+  <email>p@rdus.de</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Thomas Jarosch</name>
+  <user>jarosch</user>
+  <email>thomas.jarosch@intra2net.com</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2009-04-02</date>
+ <version>
+  <release>1.0.1</release>
+  <api>1.0.0</api>
+ </version>
+ <stability>
+  <release>stable</release>
+  <api>stable</api>
+ </stability>
+ <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+ <notes>
+   * Handle parsing errors within the DOM XML extension correctly
+     kolab/issue3520 (calendar with certain entries does not display in web client)
+     https://www.intevation.de/roundup/kolab/issue3520
+     kolab/issue3525 (free/busy regeneration aborts for unparsable events)
+     https://www.intevation.de/roundup/kolab/issue3525
+   * Accept ISO-8859-1 encoding even if advertised as UTF-8
+     kolab/issue3528 (Events with broken encoding should work)
+     https://www.intevation.de/roundup/kolab/issue3528
+ </notes>
+ <contents>
+  <dir name="/">
+   <file name="COPYING" role="doc" />
+   <dir name="doc">
+    <dir name="Horde">
+     <dir name="Kolab">
+      <dir name="Format">
+       <file name="usage.txt" role="doc" />
+      </dir> <!-- /doc/Horde/Kolab/Format -->
+     </dir> <!-- /doc/Horde/Kolab -->
+    </dir> <!-- /doc/Horde -->
+   </dir> <!-- /doc -->
+   <dir name="examples">
+    <dir name="Horde">
+     <dir name="Kolab">
+      <dir name="Format">
+       <file name="event.php" role="doc" />
+       <file name="new_type.php" role="doc" />
+      </dir> <!-- /examples/Horde/Kolab/Format -->
+     </dir> <!-- /examples/Horde/Kolab -->
+    </dir> <!-- /examples/Horde -->
+   </dir> <!-- /examples -->
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Kolab">
+      <file name="Format.php" role="php" />
+      <dir name="Format">
+       <file name="Date.php" role="php" />
+       <file name="XML.php" role="php" />
+       <dir name="XML">
+        <file name="annotation.php" role="php" />
+        <file name="contact.php" role="php" />
+        <file name="distributionlist.php" role="php" />
+        <file name="event.php" role="php" />
+        <file name="hprefs.php" role="php" />
+        <file name="note.php" role="php" />
+        <file name="task.php" role="php" />
+       </dir> <!-- /lib/Horde/Kolab/Format/XML -->
+      </dir> <!-- /lib/Horde/Kolab/Format -->
+     </dir> <!-- /lib/Horde/Kolab -->
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+   <dir name="test">
+    <dir name="Horde">
+     <dir name="Kolab">
+      <dir name="Format">
+       <file name="AllTests.php" role="test" />
+       <file name="ContactTest.php" role="test" />
+       <file name="PreferencesTest.php" role="test" />
+       <file name="RecurrenceTest.php" role="test" />
+       <file name="XmlTest.php" role="test" />
+       <dir name="fixtures">
+        <file name="contact_category.xml" role="test" />
+        <file name="contact_mail.xml" role="test" />
+        <file name="contact_pgp.xml" role="test" />
+        <file name="preferences_read_old.xml" role="test" />
+        <file name="preferences_write_old.xml" role="test" />
+        <file name="recur_fail.xml" role="test" />
+        <file name="recur.xml" role="test" />
+       </dir> <!-- /test/Horde/Kolab/Format/fixtures -->
+      </dir> <!-- /test/Horde/Kolab/Format -->
+     </dir> <!-- /test/Horde/Kolab -->
+    </dir> <!-- /test/Horde -->
+   </dir> <!-- /test -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.3.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>Horde_DOM</name>
+    <channel>pear.horde.org</channel>
+    <min>0.1.0</min>
+   </package>
+   <package>
+    <name>Horde_NLS</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Util</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+  <optional>
+   <package>
+    <name>Horde_Prefs</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Horde_Date</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </optional>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="doc/Horde/Kolab/Format/usage.txt" as="Horde/Kolab/Format/usage.txt" />
+   <install name="examples/Horde/Kolab/Format/event.php" as="Horde/Kolab/Format/event.php" />
+   <install name="examples/Horde/Kolab/Format/new_type.php" as="Horde/Kolab/Format/new_type.php" />
+   <install name="lib/Horde/Kolab/Format.php" as="Horde/Kolab/Format.php" />
+   <install name="lib/Horde/Kolab/Format/Date.php" as="Horde/Kolab/Format/Date.php" />
+   <install name="lib/Horde/Kolab/Format/XML.php" as="Horde/Kolab/Format/XML.php" />
+   <install name="lib/Horde/Kolab/Format/XML/annotation.php" as="Horde/Kolab/Format/XML/annotation.php" />
+   <install name="lib/Horde/Kolab/Format/XML/contact.php" as="Horde/Kolab/Format/XML/contact.php" />
+   <install name="lib/Horde/Kolab/Format/XML/distributionlist.php" as="Horde/Kolab/Format/XML/distributionlist.php" />
+   <install name="lib/Horde/Kolab/Format/XML/event.php" as="Horde/Kolab/Format/XML/event.php" />
+   <install name="lib/Horde/Kolab/Format/XML/hprefs.php" as="Horde/Kolab/Format/XML/hprefs.php" />
+   <install name="lib/Horde/Kolab/Format/XML/note.php" as="Horde/Kolab/Format/XML/note.php" />
+   <install name="lib/Horde/Kolab/Format/XML/task.php" as="Horde/Kolab/Format/XML/task.php" />
+   <install name="test/Horde/Kolab/Format/AllTests.php" as="Horde/Kolab/Format/AllTests.php" />
+   <install name="test/Horde/Kolab/Format/ContactTest.php" as="Horde/Kolab/Format/ContactTest.php" />
+   <install name="test/Horde/Kolab/Format/PreferencesTest.php" as="Horde/Kolab/Format/PreferencesTest.php" />
+   <install name="test/Horde/Kolab/Format/RecurrenceTest.php" as="Horde/Kolab/Format/RecurrenceTest.php" />
+   <install name="test/Horde/Kolab/Format/XmlTest.php" as="Horde/Kolab/Format/XmlTest.php" />
+   <install name="test/Horde/Kolab/Format/fixtures/contact_category.xml" as="Horde/Kolab/Format/fixtures/contact_category.xml" />
+   <install name="test/Horde/Kolab/Format/fixtures/contact_mail.xml" as="Horde/Kolab/Format/fixtures/contact_mail.xml" />
+   <install name="test/Horde/Kolab/Format/fixtures/contact_pgp.xml" as="Horde/Kolab/Format/fixtures/contact_pgp.xml" />
+   <install name="test/Horde/Kolab/Format/fixtures/preferences_read_old.xml" as="Horde/Kolab/Format/fixtures/preferences_read_old.xml" />
+   <install name="test/Horde/Kolab/Format/fixtures/preferences_write_old.xml" as="Horde/Kolab/Format/fixtures/preferences_write_old.xml" />
+   <install name="test/Horde/Kolab/Format/fixtures/recur.xml" as="Horde/Kolab/Format/fixtures/recur.xml" />
+   <install name="test/Horde/Kolab/Format/fixtures/recur_fail.xml" as="Horde/Kolab/Format/fixtures/recur_fail.xml" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <date>2008-12-12</date>
+   <version>
+    <release>1.0.0</release>
+    <api>1.0.0</api>
+   </version>
+   <stability>
+    <release>stable</release>
+    <api>stable</api>
+   </stability>
+   <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+   <notes>
+     * Fixed copyright information.
+   </notes>
+  </release>
+  <release>
+   <date>2008-11-07</date>
+   <version>
+    <release>1.0.0RC2</release>
+    <api>0.2.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+     * Added functions to provide MIME related information.
+   </notes>
+  </release>
+  <release>
+   <date>2008-10-29</date>
+   <version>
+    <release>1.0.0RC1</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+     * Fixed handling of return values from _load/_saveArray().
+     * Allowed disabling the automatic creation of categories.
+     * Merge a single mail address into the list of mail addresses.
+     * Support storing public gpg keys in the contact format.
+     * Fixed a PHP5 only check when reading XML content.
+     * Use the 'application' instead of the 'categories' element in the 
+       preferences driver.
+     * Fix category handling when no preference backend is available.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.1.2</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2008-08-01</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+     * Renamed package to Kolab_Format.
+     * Removed some unnecessary translations.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.1.1</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2008-07-29</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+    * Estimated amount of required time in tasks is a float.
+    * Only convert recurrence end of type date to a date.
+    * Fixed calls to _loadArray/_saveArray.
+    * Added experimental annotations format.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2008-07-11</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+     * Initial release.
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/AllTests.php b/framework/Kolab_Format/test/Horde/Kolab/Format/AllTests.php
new file mode 100644 (file)
index 0000000..2a75a4f
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+/**
+ * All tests for the Horde_Kolab_Format:: package.
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/AllTests.php,v 1.4 2009/01/06 17:49:23 jan Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ * Define the main method 
+ */
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Horde_Kolab_Format_AllTests::main');
+}
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+/**
+ * Combine the tests for this package.
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/AllTests.php,v 1.4 2009/01/06 17:49:23 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_AllTests {
+
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Horde_Kolab_Format');
+
+        $basedir = dirname(__FILE__);
+        $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) {
+            if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+                $pathname = $file->getPathname();
+                require $pathname;
+
+                $class = str_replace(DIRECTORY_SEPARATOR, '_',
+                                     preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname));
+                $suite->addTestSuite('Horde_Kolab_Format_' . $class);
+            }
+        }
+
+        return $suite;
+    }
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Kolab_Format_AllTests::main') {
+    Horde_Kolab_Format_AllTests::main();
+}
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/ContactTest.php b/framework/Kolab_Format/test/Horde/Kolab/Format/ContactTest.php
new file mode 100644 (file)
index 0000000..2a69c17
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+/**
+ * Test the contact XML format.
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/ContactTest.php,v 1.4 2009/01/06 17:49:23 jan Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ *  We need the unit test framework 
+ */
+require_once 'PHPUnit/Framework.php';
+
+require_once 'Horde/NLS.php';
+require_once 'Horde/Kolab/Format.php';
+require_once 'Horde/Kolab/Format/XML.php';
+require_once 'Horde/Kolab/Format/XML/contact.php';
+
+class DummyRegistry {
+    function get()
+    {
+        return 'horde';
+    }
+}
+
+class Horde_Kolab_Format_XML_contact_dummy extends Horde_Kolab_Format_XML_contact
+{
+    function _saveCreationDate($parent_node, $name, $value, $missing)
+    {
+        // Only create the creation date if it has not been set before
+        if ($missing) {
+            $value = 0;
+        }
+        return $this->_saveDefault($parent_node,
+                                   $name,
+                                   $value,
+                                   array('type' => HORDE_KOLAB_XML_TYPE_DATETIME));
+    }
+
+    function _saveModificationDate($parent_node, $name, $value, $missing)
+    {
+        // Always store now as modification date
+        return $this->_saveDefault($parent_node,
+                                   $name,
+                                   0,
+                                   array('type' => HORDE_KOLAB_XML_TYPE_DATETIME));
+    }
+}
+
+/**
+ * Test the contact XML format.
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/ContactTest.php,v 1.4 2009/01/06 17:49:23 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_ContactTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Set up testing.
+     */
+    protected function setUp()
+    {
+        NLS::setCharset('utf-8');
+    }
+
+    /**
+     * Test storing single mail addresses.
+     */
+    public function testSingleEmail()
+    {
+        $contact = &new Horde_Kolab_Format_XML_contact_dummy();
+        $object = array('uid' => '1',
+                        'full-name' => 'User Name',
+                        'email' => 'user@example.org');
+        $xml = $contact->save($object);
+        if (is_a($xml, 'PEAR_Error')) {
+            $this->assertEquals('', $xml->getMessage());
+        }
+        $expect = file_get_contents(dirname(__FILE__) . '/fixtures/contact_mail.xml');
+        $this->assertEquals($expect, $xml);
+    }
+
+    /**
+     * Test storing PGP public keys.
+     */
+    public function testPGP()
+    {
+        $contact = &new Horde_Kolab_Format_XML_contact_dummy();
+        $object = array('uid' => '1',
+                        'full-name' => 'User Name',
+                        'pgp-publickey' => 'PGP Test Key',
+                        'email' => 'user@example.org');
+        $xml = $contact->save($object);
+        if (is_a($xml, 'PEAR_Error')) {
+            $this->assertEquals('', $xml->getMessage());
+        }
+        $expect = file_get_contents(dirname(__FILE__) . '/fixtures/contact_pgp.xml');
+        $this->assertEquals($expect, $xml);
+    }
+
+    /**
+     * Test loading a contact with a category.
+     */
+    public function testCategories()
+    {
+        global $prefs;
+
+        $contact = &new Horde_Kolab_Format_XML_contact();
+        $xml = file_get_contents(dirname(__FILE__) . '/fixtures/contact_category.xml');
+        $object = $contact->load($xml);
+        if (is_a($object, 'PEAR_Error')) {
+            $this->assertEquals('', $object->getMessage());
+        }
+        $this->assertContains('Test', $object['categories']);
+
+        $prefs = 'some string';
+        $object = $contact->load($xml);
+        if (is_a($object, 'PEAR_Error')) {
+            $this->assertEquals('', $object->getMessage());
+        }
+        $this->assertContains('Test', $object['categories']);
+    }
+
+    /**
+     * Test loading a contact with a category with preferences.
+     */
+    public function testCategoriesWithPrefs()
+    {
+        @include_once 'Horde.php';
+        @include_once 'Horde/Prefs.php';
+
+        global $registry, $prefs;
+
+        if (class_exists('Prefs')) {
+            $registry = new DummyRegistry();
+            $prefs = Prefs::singleton('session');
+            /* Monkey patch to allw the value to be set. */
+            $prefs->_prefs['categories'] = array('v' => '');
+            
+            $contact = &new Horde_Kolab_Format_XML_contact();
+            $xml = file_get_contents(dirname(__FILE__) . '/fixtures/contact_category.xml');
+
+            $object = $contact->load($xml);
+            if (is_a($object, 'PEAR_Error')) {
+                $this->assertEquals('', $object->getMessage());
+            }
+            $this->assertContains('Test', $object['categories']);
+            $this->assertEquals('Test', $prefs->getValue('categories'));
+        }
+    }
+
+
+}
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/EventTest.php b/framework/Kolab_Format/test/Horde/Kolab/Format/EventTest.php
new file mode 100644 (file)
index 0000000..cd71b7b
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Test event handling within the Kolab format implementation.
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/EventTest.php,v 1.1 2009/04/02 20:07:26 wrobel Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ *  We need the unit test framework 
+ */
+require_once 'PHPUnit/Framework.php';
+
+require_once 'Horde/NLS.php';
+require_once 'Horde/Kolab/Format.php';
+
+/**
+ * Test event handling.
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/EventTest.php,v 1.1 2009/04/02 20:07:26 wrobel Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_EventTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Set up testing.
+     */
+    protected function setUp()
+    {
+        NLS::setCharset('utf-8');
+    }
+
+
+    /**
+     * Test for https://www.intevation.de/roundup/kolab/issue3525
+     */
+    public function testIssue3525()
+    {
+        $xml = Horde_Kolab_Format::factory('XML', 'event');
+        if (is_a($xml, 'PEAR_Error')) {
+            $this->assertEquals('', $xml->getMessage());
+        }
+
+        // Load XML
+        $event = file_get_contents(dirname(__FILE__) . '/fixtures/event_umlaut.xml');
+       $result = $xml->load($event);
+        // Check that the xml loads fine
+        $this->assertFalse(is_a($result, 'PEAR_Error'));
+       $this->assertEquals(mb_convert_encoding($result['body'], 'UTF-8', 'ISO-8859-1'), '...übbe...');
+
+        // Load XML
+        $event = file_get_contents(dirname(__FILE__) . '/fixtures/event_umlaut_broken.xml');
+       $result = $xml->load($event);
+        // Check that the xml loads fine
+        $this->assertFalse(is_a($result, 'PEAR_Error'));
+       //FIXME: Why does Kolab Format return ISO-8859-1? UTF-8 would seem more appropriate
+       $this->assertEquals(mb_convert_encoding($result['body'], 'UTF-8', 'ISO-8859-1'), '...übbe...');
+    }
+}
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/MimeAttrTest.php b/framework/Kolab_Format/test/Horde/Kolab/Format/MimeAttrTest.php
new file mode 100644 (file)
index 0000000..92bab67
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+/**
+ * Test Kolab Format MIME attributes
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/MimeAttrTest.php,v 1.2 2009/01/06 17:49:23 jan Exp $
+ *
+ * PHP version 5
+ *
+ * @category   Kolab
+ * @package    Kolab_Format
+ * @subpackage UnitTests
+ * @author     Gunnar Wrobel <wrobel@pardus.de>
+ * @license    http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link       http://pear.horde.org/index.php?package=Kolab_Format
+ */
+
+/**
+ *  We need the unit test framework 
+ */
+require_once 'PHPUnit/Framework.php';
+
+require_once 'Horde/NLS.php';
+require_once 'Horde/Kolab/Format.php';
+
+/**
+ * Test Kolab Format MIME attributes
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/MimeAttrTest.php,v 1.2 2009/01/06 17:49:23 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category   Kolab
+ * @package    Kolab_Format
+ * @subpackage UnitTests
+ * @author     Gunnar Wrobel <wrobel@pardus.de>
+ * @license    http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link       http://pear.horde.org/index.php?package=Kolab_Format
+ */
+class Horde_Kolab_Format_MimeAttrTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Set up testing.
+     *
+     * @return NULL
+     */
+    protected function setUp()
+    {
+        NLS::setCharset('utf-8');
+    }
+
+    /**
+     * Test retrieving the document name.
+     *
+     * @return NULL
+     */
+    public function testGetName()
+    {
+        $format = Horde_Kolab_Format::factory('XML', 'contact');
+        $this->assertEquals('kolab.xml', $format->getName());
+    }
+
+    /**
+     * Test retrieving the document mime type.
+     *
+     * @return NULL
+     */
+    public function testMimeType()
+    {
+        $format = Horde_Kolab_Format::factory('XML', 'contact');
+        $this->assertEquals('application/x-vnd.kolab.contact',
+                            $format->getMimeType());
+    }
+
+    /**
+     * Test retrieving the document disposition.
+     *
+     * @return NULL
+     */
+    public function testGetDisposition()
+    {
+        $format = Horde_Kolab_Format::factory('XML', 'contact');
+        $this->assertEquals('attachment', $format->getDisposition());
+    }
+}
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/PreferencesTest.php b/framework/Kolab_Format/test/Horde/Kolab/Format/PreferencesTest.php
new file mode 100644 (file)
index 0000000..2339af9
--- /dev/null
@@ -0,0 +1,104 @@
+<?php
+/**
+ * Test the preferences XML format.
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/PreferencesTest.php,v 1.3 2009/01/06 17:49:23 jan Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ *  We need the unit test framework 
+ */
+require_once 'PHPUnit/Framework.php';
+
+require_once 'Horde/NLS.php';
+require_once 'Horde/Kolab/Format.php';
+require_once 'Horde/Kolab/Format/XML.php';
+require_once 'Horde/Kolab/Format/XML/hprefs.php';
+
+
+class Horde_Kolab_Format_XML_hprefs_dummy extends Horde_Kolab_Format_XML_hprefs
+{
+    function _saveCreationDate($parent_node, $name, $value, $missing)
+    {
+        // Only create the creation date if it has not been set before
+        if ($missing) {
+            $value = 0;
+        }
+        return $this->_saveDefault($parent_node,
+                                   $name,
+                                   $value,
+                                   array('type' => HORDE_KOLAB_XML_TYPE_DATETIME));
+    }
+
+    function _saveModificationDate($parent_node, $name, $value, $missing)
+    {
+        // Always store now as modification date
+        return $this->_saveDefault($parent_node,
+                                   $name,
+                                   0,
+                                   array('type' => HORDE_KOLAB_XML_TYPE_DATETIME));
+    }
+}
+
+/**
+ * Test the preferences XML format.
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/PreferencesTest.php,v 1.3 2009/01/06 17:49:23 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_PreferencesTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Set up testing.
+     */
+    protected function setUp()
+    {
+        NLS::setCharset('utf-8');
+    }
+
+    /**
+     * Test preferences format conversion.
+     */
+    public function testConversionFromOld()
+    {
+        $preferences = &new Horde_Kolab_Format_XML_hprefs_dummy();
+
+        $xml = file_get_contents(dirname(__FILE__) . '/fixtures/preferences_read_old.xml');
+        $object = $preferences->load($xml);
+        if (is_a($object, 'PEAR_Error')) {
+            $this->assertEquals('', $object->getMessage());
+        }
+        $this->assertContains('test', $object['pref']);
+        $this->assertEquals('Test', $object['application']);
+
+        $object = array('uid' => 1,
+                        'pref' => array('test'),
+                        'categories' => 'Test');
+        $xml = $preferences->save($object);
+        if (is_a($xml, 'PEAR_Error')) {
+            $this->assertEquals('', $xml->getMessage());
+        }
+        $expect = file_get_contents(dirname(__FILE__) . '/fixtures/preferences_write_old.xml');
+        $this->assertEquals($expect, $xml);
+
+        $object = array('uid' => 1,
+                        'pref' => array('test'),
+                        'application' => 'Test');
+        $xml = $preferences->save($object);
+        if (is_a($xml, 'PEAR_Error')) {
+            $this->assertEquals('', $xml->getMessage());
+        }
+        $expect = file_get_contents(dirname(__FILE__) . '/fixtures/preferences_write_old.xml');
+        $this->assertEquals($expect, $xml);
+    }
+}
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/RecurrenceTest.php b/framework/Kolab_Format/test/Horde/Kolab/Format/RecurrenceTest.php
new file mode 100644 (file)
index 0000000..eb2ccfd
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+/**
+ * Test recurrence handling within the Kolab format implementation.
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/RecurrenceTest.php,v 1.10 2009/01/19 18:10:00 mrubinsk Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ *  We need the unit test framework 
+ */
+require_once 'PHPUnit/Framework.php';
+
+require_once 'Horde/NLS.php';
+require_once 'Horde/Kolab/Format.php';
+
+/**
+ * Test recurrence handling
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/RecurrenceTest.php,v 1.10 2009/01/19 18:10:00 mrubinsk Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_RecurrenceTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Set up testing.
+     */
+    protected function setUp()
+    {
+        @include_once 'Horde/Date/Recurrence.php';
+
+        if (!class_exists('Horde_Date_Recurrence')) {
+            $this->markTestSkipped(
+              'The Horde_Date_Recurrence class is missing.'
+            );
+        }
+
+        NLS::setCharset('utf-8');
+    }
+
+
+    /**
+     * Test for http://bugs.horde.org/ticket/?id=6388
+     */
+    public function testBug6388()
+    {
+        $xml = Horde_Kolab_Format::factory('XML', 'event');
+        if (is_a($xml, 'PEAR_Error')) {
+            $this->assertEquals('', $xml->getMessage());
+        }
+
+        // Load XML
+        $recur = file_get_contents(dirname(__FILE__) . '/fixtures/recur.xml');
+
+        // Check that the xml loads fine
+        $this->assertFalse(is_a($xml->load($recur), 'PEAR_Error'));
+
+        // Load XML
+        $xml = &Horde_Kolab_Format::factory('XML', 'event');
+        $recur = file_get_contents(dirname(__FILE__) . '/fixtures/recur_fail.xml');
+
+        // Check that the xml fails because of a missing interval value
+        $this->assertTrue(is_a($xml->load($recur), 'PEAR_Error'));
+    }
+
+
+    /**
+     * Test exception handling.
+     */
+    public function testExceptions()
+    {
+        $xml = Horde_Kolab_Format::factory('XML', 'event');
+        if (is_a($xml, 'PEAR_Error')) {
+            $this->assertEquals('', $xml->getMessage());
+        }
+
+        // Load XML
+        $recur = file_get_contents(dirname(__FILE__) . '/fixtures/recur.xml');
+
+        $object = $xml->load($recur);
+        if (is_a($object, 'PEAR_Error')) {
+            $this->ensureEquals('', $object->getMessage());
+        }
+
+        $r = &new Horde_Date_Recurrence($object['start-date']);
+        $r->fromHash($object['recurrence']);
+
+        $this->assertTrue($r->hasRecurEnd());
+        $this->assertTrue($r->hasException(2006,  8, 16));
+        $this->assertTrue($r->hasException(2006, 10, 18));
+
+        $object['recurrence'] = $r->toHash();
+        $recur = $xml->save($object);
+
+        $object = $xml->load($recur);
+        if (is_a($object, 'PEAR_Error')) {
+            $this->ensureEquals('', $object->getMessage());
+        }
+
+        $s = &new Horde_Date_Recurrence($object['start-date']);
+        $s->fromHash($object['recurrence']);
+
+        $this->assertTrue($s->hasRecurEnd());
+        $this->assertTrue($s->hasException(2006,  8, 16));
+        $this->assertTrue($s->hasException(2006, 10, 18));
+    }
+
+    /**
+     * Test completion handling.
+     */
+    public function testCompletions()
+    {
+        $xml = Horde_Kolab_Format::factory('XML', 'event');
+        if (is_a($xml, 'PEAR_Error')) {
+            $this->assertEquals('', $xml->getMessage());
+        }
+
+        $r = &new Horde_Date_Recurrence(0);
+        $r->setRecurType(Horde_Date_Recurrence::RECUR_DAILY);
+        $r->addException(1970, 1, 1);
+        $r->addCompletion(1970, 1, 2);
+        $r->addException(1970, 1, 3);
+        $r->addCompletion(1970, 1, 4);
+        $r->setRecurEnd(new Horde_Date(86400*3));
+
+        $object = array('uid' => 0, 'start-date' => 0, 'end-date' => 60);
+        $object['recurrence'] = $r->toHash();
+        $recur = $xml->save($object);
+
+        $object = $xml->load($recur);
+        if (is_a($object, 'PEAR_Error')) {
+            $this->ensureEquals('', $object->getMessage());
+        }
+
+        $s = &new Horde_Date_Recurrence(0);
+        $s->fromHash($object['recurrence']);
+
+        $this->assertTrue($s->hasRecurEnd());
+        $this->assertTrue($s->hasException(1970,  1, 1));
+        $this->assertTrue($s->hasCompletion(1970,  1, 2));
+        $this->assertTrue($s->hasException(1970,  1, 3));
+        $this->assertTrue($s->hasCompletion(1970,  1, 4));
+        $this->assertEquals(2, count($s->getCompletions()));
+        $this->assertEquals(2, count($s->getExceptions()));
+        $this->assertFalse($s->hasActiveRecurrence());
+
+        $s->deleteCompletion(1970, 1, 2);
+        $this->assertEquals(1, count($s->getCompletions()));
+        $s->deleteCompletion(1970, 1, 4);
+        $this->assertEquals(0, count($s->getCompletions()));
+    }
+}
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/XmlTest.php b/framework/Kolab_Format/test/Horde/Kolab/Format/XmlTest.php
new file mode 100644 (file)
index 0000000..350c508
--- /dev/null
@@ -0,0 +1,303 @@
+<?php
+/**
+ * Test the XML format implementation.
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/XmlTest.php,v 1.5 2009/01/06 17:49:23 jan Exp $
+ *
+ * @package Kolab_Format
+ */
+
+/**
+ *  We need the unit test framework 
+ */
+require_once 'PHPUnit/Framework.php';
+
+require_once 'Horde/NLS.php';
+require_once 'Horde/Kolab/Format.php';
+require_once 'Horde/Kolab/Format/XML.php';
+
+/**
+ * A dummy XML type
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/XmlTest.php,v 1.5 2009/01/06 17:49:23 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_XML_dummy extends Horde_Kolab_Format_XML
+{
+    function _saveValue($node, $name, $value, $missing)
+    {
+        $result='';
+        $result .= $name . ': ';
+        $result .= $value;
+        if ($missing) $result .= ', missing';
+
+        return $this->_saveDefault($node, 
+                                   $name, 
+                                   $result, 
+                                   array('type' => HORDE_KOLAB_XML_TYPE_STRING));
+    }
+}
+
+/**
+ * Test the XML format.
+ *
+ * $Horde: framework/Kolab_Format/test/Horde/Kolab/Format/XmlTest.php,v 1.5 2009/01/06 17:49:23 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_Format
+ */
+class Horde_Kolab_Format_XmlTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Set up testing.
+     */
+    protected function setUp()
+    {
+        NLS::setCharset('utf-8');
+    }
+
+
+    /**
+     * Check the preparation of the basic XML structure
+     */
+    public function testBasic()
+    {
+        $xml = &new Horde_Kolab_Format_XML();
+        $xml->_prepareSave();
+        $base = $xml->_xmldoc->dump_mem(true);
+        $this->assertEquals("<?xml version=\"1.0\"?>\n<kolab version=\"1.0\"/>\n", $base);
+    }
+
+    /**
+     * The resulting XML string should be readable.
+     */
+    public function testReadable()
+    {
+        $xml = &new Horde_Kolab_Format_XML();
+        $xml->_prepareSave();
+        $base = $xml->_xmldoc->dump_mem(true);
+        $xml->_parseXml($base);
+        $this->assertEquals($base, $xml->_xmldoc->dump_mem(true));
+
+    }
+
+    /**
+     * Test adding nodes.
+     */
+    public function testAdd()
+    {
+        $xml = &new Horde_Kolab_Format_XML();
+        $xml->_prepareSave();
+        $root = $xml->_xmldoc->document_element();
+        $base = $xml->_xmldoc->dump_mem(true);
+
+        // A missing attribute should cause no change if it
+        // is allowed to be empty
+        $xml->_updateNode($root,
+                          array(),
+                          'empty1',
+                          array('value' => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING));
+        $this->assertEquals($base, $xml->_xmldoc->dump_mem(true));
+
+        // A missing attribute should cause an error if it
+        // is not allowed to be empty
+        $this->assertTrue(is_a($xml->_updateNode($root,
+                                                array(),
+                                                'empty1',
+                                                array('value' => HORDE_KOLAB_XML_VALUE_NOT_EMPTY)), 'PEAR_Error'));
+
+        $xml->_updateNode($root,
+                         array(),
+                         'empty1',
+                         array('value' => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                               'default' => 'empty1', 'type' => 0));
+        $this->assertEquals("<?xml version=\"1.0\"?>\n<kolab version=\"1.0\">\n  <empty1>empty1</empty1>\n</kolab>\n",  $xml->_xmldoc->dump_mem(true));
+
+        $this->assertTrue(is_a($xml->_updateNode($root,
+                                                array(),
+                                                'empty1',
+                                                array('value' => HORDE_KOLAB_XML_VALUE_CALCULATED,
+                                                      'save' => '_unknown')), 'PEAR_Error'));
+    }
+
+
+    /**
+     * Test node operations
+     */
+    public function testNodeOps()
+    {
+        $dxml = new Horde_Kolab_Format_XML_dummy();
+        $dxml->_prepareSave();
+        $droot = $dxml->_xmldoc->document_element();
+
+        // Test calculated nodes
+        $dxml->_updateNode($droot,
+                          array(),
+                          'empty2',
+                          array('value' => HORDE_KOLAB_XML_VALUE_CALCULATED,
+                                'save' => 'Value', 'type' => 0));
+        $dxml->_updateNode($droot,
+                          array('present1' => 'present1'),
+                          'present1',
+                          array('value' => HORDE_KOLAB_XML_VALUE_CALCULATED,
+                                'save' => 'Value', 'type' => 0));
+        $this->assertEquals("<?xml version=\"1.0\"?>\n<kolab version=\"1.0\">\n  <empty2>empty2: , missing</empty2>\n  <present1>present1: present1</present1>\n</kolab>\n",  $dxml->_xmldoc->dump_mem(true));
+
+        $xml = &new Horde_Kolab_Format_XML();
+        $xml->_prepareSave();
+        $root = $xml->_xmldoc->document_element();
+        $xml->_updateNode($root,
+                         array(),
+                         'empty1',
+                         array('value' => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                               'default' => 'empty1', 'type' => 0));
+
+        // Back to the original object: Test saving a normal value
+        $xml->_updateNode($root,
+                         array('present1' => 'present1'),
+                         'present1',
+                         array('value' => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                               'default' => 'empty1', 'type' => 0));
+        $this->assertEquals("<?xml version=\"1.0\"?>\n<kolab version=\"1.0\">\n  <empty1>empty1</empty1>\n  <present1>present1</present1>\n</kolab>\n",  $xml->_xmldoc->dump_mem(true));
+
+        // Test overwriting a value
+        $xml->_updateNode($root,
+                         array('present1' => 'new1'),
+                         'present1',
+                         array('value' => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                               'default' => 'empty1', 'type' => 0));
+        $this->assertEquals("<?xml version=\"1.0\"?>\n<kolab version=\"1.0\">\n  <empty1>empty1</empty1>\n  <present1>new1</present1>\n</kolab>\n",  $xml->_xmldoc->dump_mem(true));
+
+        // Test saving a date
+        $xml->_updateNode($root,
+                         array('date1' => 1175080008),
+                         'date1',
+                         array('value' => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                               'default' => 'empty1', 
+                               'type' => HORDE_KOLAB_XML_TYPE_DATETIME));
+        $this->assertEquals("<?xml version=\"1.0\"?>\n<kolab version=\"1.0\">\n  <empty1>empty1</empty1>\n  <present1>new1</present1>\n  <date1>2007-03-28T11:06:48Z</date1>\n</kolab>\n",  $xml->_xmldoc->dump_mem(true));
+
+        // Now load the data back in
+        $children = $root->child_nodes();
+
+        // Test loading a value that may be empty
+        $this->assertEquals(null, $xml->_getXmlData($children,
+                                                   'empty2',
+                                                   array('value' => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+                                                         'default' => '', 
+                                                         'type' => HORDE_KOLAB_XML_TYPE_STRING)));
+
+        // Test loading a value that may not be empty
+        $this->assertTrue(is_a($xml->_getXmlData($children,
+                                                'empty2',
+                                                array('value' => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
+                                                      'default' => '', 
+                                                      'type' => HORDE_KOLAB_XML_TYPE_STRING)), 'PEAR_Error'));
+
+        // Test loading a missing value with a default
+        $this->assertEquals(0 ,$xml->_getXmlData($children,
+                                                'date2',
+                                                array('value' => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                                                      'default' => 0, 
+                                                      'type' => HORDE_KOLAB_XML_TYPE_DATETIME)));
+
+        // Test loading a calculated value
+        $this->assertEquals('new1', $dxml->_getXmlData($children,
+                                                      'present1',
+                                                      array('value' => HORDE_KOLAB_XML_VALUE_CALCULATED,
+                                                            'func' => '_calculate',
+                                                            'type' => HORDE_KOLAB_XML_TYPE_STRING)));
+
+        // Test loading a normal value
+        $this->assertEquals('new1', $xml->_getXmlData($children,
+                                                     'present1',
+                                                     array('value' => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                                                           'default' => 'empty',
+                                                           'type' => HORDE_KOLAB_XML_TYPE_STRING)));
+
+        // Test loading a date value
+        $this->assertEquals(1175080008, $xml->_getXmlData($children,
+                                                          'date1',
+                                                          array('value' => HORDE_KOLAB_XML_VALUE_DEFAULT,
+                                                                'default' => 0,
+                                                                'type' => HORDE_KOLAB_XML_TYPE_DATETIME)));
+    }
+
+
+    /**
+     * Test load/save
+     */
+    public function testReleod()
+    {
+        // Save an object and reload it
+        $xml = new Horde_Kolab_Format_XML();
+        $result = $xml->save(array('uid'=>'test',
+                                   'body' => 'body',
+                                   'dummy' => 'hello',
+                                   'creation-date' => 1175080008,
+                                   'last-modification-date' => 1175080008,
+                             ));
+        $object = $xml->load($result);
+        $this->assertEquals('body', $object['body']);
+        $this->assertTrue(empty($object['dummy']));
+        $this->assertEquals('public', $object['sensitivity']);
+        $this->assertEquals(1175080008, $object['creation-date']);
+        $this->assertTrue($object['last-modification-date'] != 1175080008);
+        $this->assertEquals('Horde::Kolab', $object['product-id']);
+    }
+
+    /**
+     * Test complex values
+     */
+    public function testComplex()
+    {
+        // Continue with complex values
+        $xml = new Horde_Kolab_Format_XML();
+        $xml->_prepareSave();
+        $root = $xml->_xmldoc->document_element();
+
+        // Test saving a composite value
+        $xml->_updateNode($root,
+                         array('composite1' => array('display-name' => 'test', 'smtp-address' => 'test@example.com')),
+                         'composite1', $xml->_fields_simple_person);
+        $this->assertEquals("<?xml version=\"1.0\"?>\n<kolab version=\"1.0\">\n  <composite1>\n    <display-name>test</display-name>\n    <smtp-address>test@example.com</smtp-address>\n    <uid></uid>\n  </composite1>\n</kolab>\n",  $xml->_xmldoc->dump_mem(true));
+
+        // Test saving multiple values
+        $xml->_updateNode($root,
+                         array('attendee1' => array(array('display-name' => 'test'), array('smtp-address' => 'test@example.com'))),
+                         'attendee1', $xml->_fields_attendee);
+        $this->assertEquals("<?xml version=\"1.0\"?>\n<kolab version=\"1.0\">\n  <composite1>\n    <display-name>test</display-name>\n    <smtp-address>test@example.com</smtp-address>\n    <uid></uid>\n  </composite1>\n  <attendee1>\n    <display-name>test</display-name>\n    <smtp-address></smtp-address>\n    <status>none</status>\n    <request-response>true</request-response>\n    <role>required</role>\n  </attendee1>\n  <attendee1>\n    <display-name></display-name>\n    <smtp-address>test@example.com</smtp-address>\n    <status>none</status>\n    <request-response>true</request-response>\n    <role>required</role>\n  </attendee1>\n</kolab>\n",  $xml->_xmldoc->dump_mem(true));
+
+        $children = $root->child_nodes();
+
+        // Load a composite value
+        $data = $xml->_getXmlData($children,
+                                  'composite1', 
+                                  $xml->_fields_simple_person);
+
+        $this->assertEquals(3, count($data));
+        $this->assertEquals('test@example.com', $data['smtp-address']);
+
+        // Load multiple values
+        $data = $xml->_getXmlData($children,
+                                  'attendee1', 
+                                  $xml->_fields_attendee);
+        $this->assertEquals(2, count($data));
+        $this->assertEquals(5, count($data[0]));
+        $this->assertEquals('', $data[0]['smtp-address']);
+        $this->assertEquals('test@example.com', $data[1]['smtp-address']);
+    }
+}
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_category.xml b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_category.xml
new file mode 100644 (file)
index 0000000..0fb7c0e
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<contact version="1.0">
+  <uid>1</uid>
+  <body></body>
+  <categories>Test</categories>
+  <creation-date>1970-01-01T00:00:00Z</creation-date>
+  <last-modification-date>1970-01-01T00:00:00Z</last-modification-date>
+  <sensitivity>public</sensitivity>
+  <product-id>Horde::Kolab</product-id>
+  <name>
+    <full-name>User Name</full-name>
+  </name>
+  <email>
+    <display-name>User Name</display-name>
+    <smtp-address>user@example.org</smtp-address>
+    <uid></uid>
+  </email>
+</contact>
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_mail.xml b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_mail.xml
new file mode 100644 (file)
index 0000000..af04840
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<contact version="1.0">
+  <uid>1</uid>
+  <body></body>
+  <categories></categories>
+  <creation-date>1970-01-01T00:00:00Z</creation-date>
+  <last-modification-date>1970-01-01T00:00:00Z</last-modification-date>
+  <sensitivity>public</sensitivity>
+  <product-id>Horde::Kolab</product-id>
+  <name>
+    <full-name>User Name</full-name>
+  </name>
+  <email>
+    <display-name>User Name</display-name>
+    <smtp-address>user@example.org</smtp-address>
+    <uid></uid>
+  </email>
+</contact>
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_pgp.xml b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_pgp.xml
new file mode 100644 (file)
index 0000000..7be0574
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<contact version="1.0">
+  <uid>1</uid>
+  <body></body>
+  <categories></categories>
+  <creation-date>1970-01-01T00:00:00Z</creation-date>
+  <last-modification-date>1970-01-01T00:00:00Z</last-modification-date>
+  <sensitivity>public</sensitivity>
+  <product-id>Horde::Kolab</product-id>
+  <name>
+    <full-name>User Name</full-name>
+  </name>
+  <email>
+    <display-name>User Name</display-name>
+    <smtp-address>user@example.org</smtp-address>
+    <uid></uid>
+  </email>
+  <pgp-publickey>PGP Test Key</pgp-publickey>
+</contact>
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/event_umlaut.xml b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/event_umlaut.xml
new file mode 100644 (file)
index 0000000..a00d2cd
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<event version="1.0" >
+ <product-id>KOrganizer 3.3 (proko2 branch after 2.1.5), Kolab resource</product-id>
+ <uid>libkcal-543769073.139</uid>
+ <creation-date>2006-03-16T15:00:53Z</creation-date>
+ <last-modification-date>2007-01-25T11:36:40Z</last-modification-date>
+ <sensitivity>public</sensitivity>
+ <pilot-sync-status>1</pilot-sync-status>
+ <start-date>2006-03-15T18:30:00Z</start-date>
+ <summary>Summary</summary>
+ <organizer>
+  <display-name>Orga Nizer</display-name>
+  <smtp-address>orga.nizer@example.com</smtp-address>
+ </organizer>
+ <body>...übbe...</body>
+ <revision>0</revision>
+ <show-time-as>busy</show-time-as>
+ <end-date>2007-03-15T20:00:00Z</end-date>
+</event>
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/event_umlaut_broken.xml b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/event_umlaut_broken.xml
new file mode 100644 (file)
index 0000000..dd6893f
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<event version="1.0" >
+ <product-id>KOrganizer 3.3 (proko2 branch after 2.1.5), Kolab resource</product-id>
+ <uid>libkcal-543769073.139</uid>
+ <creation-date>2006-03-16T15:00:53Z</creation-date>
+ <last-modification-date>2007-01-25T11:36:40Z</last-modification-date>
+ <sensitivity>public</sensitivity>
+ <pilot-sync-status>1</pilot-sync-status>
+ <start-date>2006-03-15T18:30:00Z</start-date>
+ <summary>Summary</summary>
+ <organizer>
+  <display-name>Orga Nizer</display-name>
+  <smtp-address>orga.nizer@example.com</smtp-address>
+ </organizer>
+ <body>...übbe...</body>
+ <revision>0</revision>
+ <show-time-as>busy</show-time-as>
+ <end-date>2007-03-15T20:00:00Z</end-date>
+</event>
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/preferences_read_old.xml b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/preferences_read_old.xml
new file mode 100644 (file)
index 0000000..c7ede5c
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<contact version="1.0">
+  <uid>1</uid>
+  <body></body>
+  <creation-date>1970-01-01T00:00:00Z</creation-date>
+  <last-modification-date>1970-01-01T00:00:00Z</last-modification-date>
+  <sensitivity>public</sensitivity>
+  <product-id>Horde::Kolab</product-id>
+  <categories>Test</categories>
+  <pref>test</pref>
+</contact>
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/preferences_write_old.xml b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/preferences_write_old.xml
new file mode 100644 (file)
index 0000000..d255ce2
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<contact version="1.0">
+  <uid>1</uid>
+  <body></body>
+  <categories></categories>
+  <creation-date>1970-01-01T00:00:00Z</creation-date>
+  <last-modification-date>1970-01-01T00:00:00Z</last-modification-date>
+  <sensitivity>public</sensitivity>
+  <product-id>Horde::Kolab</product-id>
+  <application>Test</application>
+  <pref>test</pref>
+</contact>
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/recur.xml b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/recur.xml
new file mode 100644 (file)
index 0000000..63a6b5f
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<event version="1.0" >
+ <product-id>KOrganizer 3.3 (proko2 branch after 2.1.5), Kolab resource</product-id>
+ <uid>libkcal-543769073.139</uid>
+ <creation-date>2006-03-16T15:00:53Z</creation-date>
+ <last-modification-date>2007-01-25T11:36:40Z</last-modification-date>
+ <sensitivity>public</sensitivity>
+ <pilot-sync-status>1</pilot-sync-status>
+ <start-date>2006-03-15T18:30:00Z</start-date>
+ <summary>Summary</summary>
+ <organizer>
+  <display-name>Orga Nizer</display-name>
+  <smtp-address>orga.nizer@example.com</smtp-address>
+ </organizer>
+ <recurrence cycle="weekly" >
+  <interval>1</interval>
+  <day>wednesday</day>
+  <range type="date" >2007-01-24</range>
+  <exclusion>2006-04-05</exclusion>
+  <exclusion>2006-04-12</exclusion>
+  <exclusion>2006-07-19</exclusion>
+  <exclusion>2006-07-26</exclusion>
+  <exclusion>2006-08-02</exclusion>
+  <exclusion>2006-08-09</exclusion>
+  <exclusion>2006-08-16</exclusion>
+  <exclusion>2006-08-23</exclusion>
+  <exclusion>2006-07-12</exclusion>
+  <exclusion>2006-09-06</exclusion>
+  <exclusion>2006-09-13</exclusion>
+  <exclusion>2006-10-18</exclusion>
+  <exclusion>2006-10-25</exclusion>
+  <exclusion>2006-12-27</exclusion>
+  <exclusion>2007-01-17</exclusion>
+  <exclusion>2007-01-10</exclusion>
+  <exclusion>2007-01-03</exclusion>
+ </recurrence>
+ <revision>0</revision>
+ <show-time-as>busy</show-time-as>
+ <end-date>2007-03-15T20:00:00Z</end-date>
+</event>
diff --git a/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/recur_fail.xml b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/recur_fail.xml
new file mode 100644 (file)
index 0000000..4c46dd2
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<event version="1.0" >
+ <product-id>KOrganizer 3.3 (proko2 branch after 2.1.5), Kolab resource</product-id>
+ <uid>libkcal-543769073.139</uid>
+ <creation-date>2006-03-16T15:00:53Z</creation-date>
+ <last-modification-date>2007-01-25T11:36:40Z</last-modification-date>
+ <sensitivity>public</sensitivity>
+ <pilot-sync-status>1</pilot-sync-status>
+ <start-date>2006-03-15T18:30:00Z</start-date>
+ <summary>Summary</summary>
+ <organizer>
+  <display-name>Orga Nizer</display-name>
+  <smtp-address>orga.nizer@example.com</smtp-address>
+ </organizer>
+ <recurrence>
+  <day>wednesday</day>
+  <range type="date" >2007-01-24</range>
+  <exclusion>2006-04-05</exclusion>
+  <exclusion>2006-04-12</exclusion>
+  <exclusion>2006-07-19</exclusion>
+  <exclusion>2006-07-26</exclusion>
+  <exclusion>2006-08-02</exclusion>
+  <exclusion>2006-08-09</exclusion>
+  <exclusion>2006-08-16</exclusion>
+  <exclusion>2006-08-23</exclusion>
+  <exclusion>2006-07-12</exclusion>
+  <exclusion>2006-09-06</exclusion>
+  <exclusion>2006-09-13</exclusion>
+  <exclusion>2006-10-18</exclusion>
+  <exclusion>2006-10-25</exclusion>
+  <exclusion>2006-12-27</exclusion>
+  <exclusion>2007-01-17</exclusion>
+  <exclusion>2007-01-10</exclusion>
+  <exclusion>2007-01-03</exclusion>
+ </recurrence>
+ <revision>0</revision>
+ <show-time-as>busy</show-time-as>
+ <end-date>2006-03-15T20:00:00Z</end-date>
+</event>