From 50c79a502e61dcd476c45e09a8ff29b8c17e58aa Mon Sep 17 00:00:00 2001
From: Gunnar Wrobel
Date: Tue, 23 Jun 2009 18:06:12 +0200
Subject: [PATCH] Imported Kolab_Format from Horde CVS.
---
framework/Kolab_Format/COPYING | 504 +++++++
.../Kolab_Format/doc/Horde/Kolab/Format/usage.txt | 339 +++++
.../examples/Horde/Kolab/Format/event.php | 31 +
.../examples/Horde/Kolab/Format/new_type.php | 74 +
framework/Kolab_Format/lib/Horde/Kolab/Format.php | 58 +
.../Kolab_Format/lib/Horde/Kolab/Format/Date.php | 113 ++
.../Kolab_Format/lib/Horde/Kolab/Format/XML.php | 1468 ++++++++++++++++++++
.../lib/Horde/Kolab/Format/XML/annotation.php | 101 ++
.../lib/Horde/Kolab/Format/XML/contact.php | 519 +++++++
.../Horde/Kolab/Format/XML/distributionlist.php | 109 ++
.../lib/Horde/Kolab/Format/XML/event.php | 138 ++
.../lib/Horde/Kolab/Format/XML/hprefs.php | 109 ++
.../lib/Horde/Kolab/Format/XML/note.php | 102 ++
.../lib/Horde/Kolab/Format/XML/task.php | 191 +++
framework/Kolab_Format/package.xml | 286 ++++
.../test/Horde/Kolab/Format/AllTests.php | 64 +
.../test/Horde/Kolab/Format/ContactTest.php | 162 +++
.../test/Horde/Kolab/Format/EventTest.php | 68 +
.../test/Horde/Kolab/Format/MimeAttrTest.php | 88 ++
.../test/Horde/Kolab/Format/PreferencesTest.php | 104 ++
.../test/Horde/Kolab/Format/RecurrenceTest.php | 161 +++
.../test/Horde/Kolab/Format/XmlTest.php | 303 ++++
.../Kolab/Format/fixtures/contact_category.xml | 18 +
.../Horde/Kolab/Format/fixtures/contact_mail.xml | 18 +
.../Horde/Kolab/Format/fixtures/contact_pgp.xml | 19 +
.../Horde/Kolab/Format/fixtures/event_umlaut.xml | 19 +
.../Kolab/Format/fixtures/event_umlaut_broken.xml | 19 +
.../Kolab/Format/fixtures/preferences_read_old.xml | 11 +
.../Format/fixtures/preferences_write_old.xml | 12 +
.../test/Horde/Kolab/Format/fixtures/recur.xml | 40 +
.../Horde/Kolab/Format/fixtures/recur_fail.xml | 39 +
31 files changed, 5287 insertions(+)
create mode 100644 framework/Kolab_Format/COPYING
create mode 100644 framework/Kolab_Format/doc/Horde/Kolab/Format/usage.txt
create mode 100644 framework/Kolab_Format/examples/Horde/Kolab/Format/event.php
create mode 100644 framework/Kolab_Format/examples/Horde/Kolab/Format/new_type.php
create mode 100644 framework/Kolab_Format/lib/Horde/Kolab/Format.php
create mode 100644 framework/Kolab_Format/lib/Horde/Kolab/Format/Date.php
create mode 100644 framework/Kolab_Format/lib/Horde/Kolab/Format/XML.php
create mode 100644 framework/Kolab_Format/lib/Horde/Kolab/Format/XML/annotation.php
create mode 100644 framework/Kolab_Format/lib/Horde/Kolab/Format/XML/contact.php
create mode 100644 framework/Kolab_Format/lib/Horde/Kolab/Format/XML/distributionlist.php
create mode 100644 framework/Kolab_Format/lib/Horde/Kolab/Format/XML/event.php
create mode 100644 framework/Kolab_Format/lib/Horde/Kolab/Format/XML/hprefs.php
create mode 100644 framework/Kolab_Format/lib/Horde/Kolab/Format/XML/note.php
create mode 100644 framework/Kolab_Format/lib/Horde/Kolab/Format/XML/task.php
create mode 100644 framework/Kolab_Format/package.xml
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/AllTests.php
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/ContactTest.php
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/EventTest.php
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/MimeAttrTest.php
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/PreferencesTest.php
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/RecurrenceTest.php
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/XmlTest.php
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_category.xml
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_mail.xml
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_pgp.xml
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/event_umlaut.xml
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/event_umlaut_broken.xml
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/preferences_read_old.xml
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/preferences_write_old.xml
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/recur.xml
create mode 100644 framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/recur_fail.xml
diff --git a/framework/Kolab_Format/COPYING b/framework/Kolab_Format/COPYING
new file mode 100644
index 000000000..d1c6b98aa
--- /dev/null
+++ b/framework/Kolab_Format/COPYING
@@ -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.
+
+ 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.
+
+ 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.
+
+ 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.
+
+ 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.
+
+ 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.
+
+ 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.
+
+ 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.
+
+ 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
+
+ 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.
+
+
+ Copyright
+
+ 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.
+
+ , 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
index 000000000..e459f6bb0
--- /dev/null
+++ b/framework/Kolab_Format/doc/Horde/Kolab/Format/usage.txt
@@ -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 $format 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 $xml and
+$read_object this will be the result:
+
+ var_dump($xml);
+ string(438) "
+
+ 1
+
+
+ 2008-07-10T12:51:51Z
+ 2008-07-10T12:51:51Z
+ public
+ Horde::Kolab
+ test event
+ 2008-07-10T12:51:51Z
+ 2008-07-11T12:51:51Z
+
+ "
+
+ 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) "
+
+ 1
+
+
+ 2008-07-10T13:28:36Z
+ 2008-07-10T13:28:36Z
+ public
+ Horde::Kolab
+ test 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
index 000000000..f3bb6cbce
--- /dev/null
+++ b/framework/Kolab_Format/examples/Horde/Kolab/Format/event.php
@@ -0,0 +1,31 @@
+ 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
index 000000000..1cd2a9f07
--- /dev/null
+++ b/framework/Kolab_Format/examples/Horde/Kolab/Format/new_type.php
@@ -0,0 +1,74 @@
+
+ * @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
index 000000000..d2dd7e526
--- /dev/null
+++ b/framework/Kolab_Format/lib/Horde/Kolab/Format.php
@@ -0,0 +1,58 @@
+
+ * @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
index 000000000..8a1a622f7
--- /dev/null
+++ b/framework/Kolab_Format/lib/Horde/Kolab/Format/Date.php
@@ -0,0 +1,113 @@
+
+ * @author Thomas Jarosch
+ * @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
index 000000000..755e9a6d2
--- /dev/null
+++ b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML.php
@@ -0,0 +1,1468 @@
+
+ * @author Gunnar Wrobel
+ * @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
+ 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
+ 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
index 000000000..77c733dba
--- /dev/null
+++ b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/annotation.php
@@ -0,0 +1,101 @@
+
+ * @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
index 000000000..ffae14454
--- /dev/null
+++ b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/contact.php
@@ -0,0 +1,519 @@
+
+ * @author Gunnar Wrobel
+ * @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
index 000000000..b81573fdd
--- /dev/null
+++ b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/distributionlist.php
@@ -0,0 +1,109 @@
+
+ * @author Gunnar Wrobel
+ * @author Mike Gabriel
+ * @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
index 000000000..24fc8b948
--- /dev/null
+++ b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/event.php
@@ -0,0 +1,138 @@
+
+ * @author Gunnar Wrobel
+ * @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
index 000000000..58b0137ce
--- /dev/null
+++ b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/hprefs.php
@@ -0,0 +1,109 @@
+
+ * @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
index 000000000..bb69c2e20
--- /dev/null
+++ b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/note.php
@@ -0,0 +1,102 @@
+
+ * @author Gunnar Wrobel
+ * @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
index 000000000..50d516f9b
--- /dev/null
+++ b/framework/Kolab_Format/lib/Horde/Kolab/Format/XML/task.php
@@ -0,0 +1,191 @@
+
+ * @author Gunnar Wrobel
+ * @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
index 000000000..abd00cebc
--- /dev/null
+++ b/framework/Kolab_Format/package.xml
@@ -0,0 +1,286 @@
+
+
+ Kolab_Format
+ pear.horde.org
+ A package for reading/writing Kolab data formats
+ This package allows to convert Kolab data objects from
+ XML to hashes.
+
+ Gunnar Wrobel
+ wrobel
+ p@rdus.de
+ yes
+
+
+ Thomas Jarosch
+ jarosch
+ thomas.jarosch@intra2net.com
+ yes
+
+
+ Chuck Hagenbuch
+ chuck
+ chuck@horde.org
+ yes
+
+
+ Jan Schneider
+ jan
+ jan@horde.org
+ yes
+
+ 2009-04-02
+
+ 1.0.1
+ 1.0.0
+
+
+ stable
+ stable
+
+ LGPL
+
+ * 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 4.3.0
+
+
+ 1.4.0b1
+
+
+ Horde_DOM
+ pear.horde.org
+ 0.1.0
+
+
+ Horde_NLS
+ pear.horde.org
+
+
+ Util
+ pear.horde.org
+
+
+
+
+ Horde_Prefs
+ pear.horde.org
+
+
+ Horde_Date
+ pear.horde.org
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2008-12-12
+
+ 1.0.0
+ 1.0.0
+
+
+ stable
+ stable
+
+ LGPL
+
+ * Fixed copyright information.
+
+
+
+ 2008-11-07
+
+ 1.0.0RC2
+ 0.2.0
+
+
+ beta
+ alpha
+
+ LGPL
+
+ * Added functions to provide MIME related information.
+
+
+
+ 2008-10-29
+
+ 1.0.0RC1
+ 0.1.0
+
+
+ beta
+ alpha
+
+ LGPL
+
+ * 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.
+
+
+
+
+ 0.1.2
+ 0.1.0
+
+
+ alpha
+ alpha
+
+ 2008-08-01
+ LGPL
+
+ * Renamed package to Kolab_Format.
+ * Removed some unnecessary translations.
+
+
+
+
+ 0.1.1
+ 0.1.0
+
+
+ alpha
+ alpha
+
+ 2008-07-29
+ LGPL
+
+ * 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.
+
+
+
+
+ 0.1.0
+ 0.1.0
+
+
+ alpha
+ alpha
+
+ 2008-07-11
+ LGPL
+
+ * Initial release.
+
+
+
+
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
index 000000000..2a75a4f02
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/AllTests.php
@@ -0,0 +1,64 @@
+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
index 000000000..2a69c1763
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/ContactTest.php
@@ -0,0 +1,162 @@
+_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
+ * @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
index 000000000..cd71b7b9c
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/EventTest.php
@@ -0,0 +1,68 @@
+
+ * @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
index 000000000..92bab6791
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/MimeAttrTest.php
@@ -0,0 +1,88 @@
+
+ * @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
+ * @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
index 000000000..2339af901
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/PreferencesTest.php
@@ -0,0 +1,104 @@
+_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
+ * @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
index 000000000..eb2ccfd4e
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/RecurrenceTest.php
@@ -0,0 +1,161 @@
+
+ * @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
index 000000000..350c5083f
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/XmlTest.php
@@ -0,0 +1,303 @@
+
+ * @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
+ * @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("\n\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("\n\n empty1\n\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("\n\n empty2: , missing\n present1: present1\n\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("\n\n empty1\n present1\n\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("\n\n empty1\n new1\n\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("\n\n empty1\n new1\n 2007-03-28T11:06:48Z\n\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("\n\n \n test\n test@example.com\n \n \n\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("\n\n \n test\n test@example.com\n \n \n \n test\n \n none\n true\n required\n \n \n \n test@example.com\n none\n true\n required\n \n\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
index 000000000..0fb7c0eec
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_category.xml
@@ -0,0 +1,18 @@
+
+
+ 1
+
+ Test
+ 1970-01-01T00:00:00Z
+ 1970-01-01T00:00:00Z
+ public
+ Horde::Kolab
+
+ User Name
+
+
+ User Name
+ user@example.org
+
+
+
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
index 000000000..af0484044
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_mail.xml
@@ -0,0 +1,18 @@
+
+
+ 1
+
+
+ 1970-01-01T00:00:00Z
+ 1970-01-01T00:00:00Z
+ public
+ Horde::Kolab
+
+ User Name
+
+
+ User Name
+ user@example.org
+
+
+
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
index 000000000..7be05745d
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/contact_pgp.xml
@@ -0,0 +1,19 @@
+
+
+ 1
+
+
+ 1970-01-01T00:00:00Z
+ 1970-01-01T00:00:00Z
+ public
+ Horde::Kolab
+
+ User Name
+
+
+ User Name
+ user@example.org
+
+
+ PGP Test Key
+
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
index 000000000..a00d2cd81
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/event_umlaut.xml
@@ -0,0 +1,19 @@
+
+
+ KOrganizer 3.3 (proko2 branch after 2.1.5), Kolab resource
+ libkcal-543769073.139
+ 2006-03-16T15:00:53Z
+ 2007-01-25T11:36:40Z
+ public
+ 1
+ 2006-03-15T18:30:00Z
+ Summary
+
+ Orga Nizer
+ orga.nizer@example.com
+
+ ...übbe...
+ 0
+ busy
+ 2007-03-15T20:00:00Z
+
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
index 000000000..dd6893fee
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/event_umlaut_broken.xml
@@ -0,0 +1,19 @@
+
+
+ KOrganizer 3.3 (proko2 branch after 2.1.5), Kolab resource
+ libkcal-543769073.139
+ 2006-03-16T15:00:53Z
+ 2007-01-25T11:36:40Z
+ public
+ 1
+ 2006-03-15T18:30:00Z
+ Summary
+
+ Orga Nizer
+ orga.nizer@example.com
+
+ ...übbe...
+ 0
+ busy
+ 2007-03-15T20:00:00Z
+
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
index 000000000..c7ede5c19
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/preferences_read_old.xml
@@ -0,0 +1,11 @@
+
+
+ 1
+
+ 1970-01-01T00:00:00Z
+ 1970-01-01T00:00:00Z
+ public
+ Horde::Kolab
+ Test
+ test
+
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
index 000000000..d255ce27e
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/preferences_write_old.xml
@@ -0,0 +1,12 @@
+
+
+ 1
+
+
+ 1970-01-01T00:00:00Z
+ 1970-01-01T00:00:00Z
+ public
+ Horde::Kolab
+ Test
+ test
+
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
index 000000000..63a6b5f62
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/recur.xml
@@ -0,0 +1,40 @@
+
+
+ KOrganizer 3.3 (proko2 branch after 2.1.5), Kolab resource
+ libkcal-543769073.139
+ 2006-03-16T15:00:53Z
+ 2007-01-25T11:36:40Z
+ public
+ 1
+ 2006-03-15T18:30:00Z
+ Summary
+
+ Orga Nizer
+ orga.nizer@example.com
+
+
+ 1
+ wednesday
+ 2007-01-24
+ 2006-04-05
+ 2006-04-12
+ 2006-07-19
+ 2006-07-26
+ 2006-08-02
+ 2006-08-09
+ 2006-08-16
+ 2006-08-23
+ 2006-07-12
+ 2006-09-06
+ 2006-09-13
+ 2006-10-18
+ 2006-10-25
+ 2006-12-27
+ 2007-01-17
+ 2007-01-10
+ 2007-01-03
+
+ 0
+ busy
+ 2007-03-15T20:00:00Z
+
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
index 000000000..4c46dd27d
--- /dev/null
+++ b/framework/Kolab_Format/test/Horde/Kolab/Format/fixtures/recur_fail.xml
@@ -0,0 +1,39 @@
+
+
+ KOrganizer 3.3 (proko2 branch after 2.1.5), Kolab resource
+ libkcal-543769073.139
+ 2006-03-16T15:00:53Z
+ 2007-01-25T11:36:40Z
+ public
+ 1
+ 2006-03-15T18:30:00Z
+ Summary
+
+ Orga Nizer
+ orga.nizer@example.com
+
+
+ wednesday
+ 2007-01-24
+ 2006-04-05
+ 2006-04-12
+ 2006-07-19
+ 2006-07-26
+ 2006-08-02
+ 2006-08-09
+ 2006-08-16
+ 2006-08-23
+ 2006-07-12
+ 2006-09-06
+ 2006-09-13
+ 2006-10-18
+ 2006-10-25
+ 2006-12-27
+ 2007-01-17
+ 2007-01-10
+ 2007-01-03
+
+ 0
+ busy
+ 2006-03-15T20:00:00Z
+
--
2.11.0