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