Imported Kolab_FreeBusy from CVS.
authorGunnar Wrobel <p@rdus.de>
Wed, 9 Sep 2009 07:06:10 +0000 (09:06 +0200)
committerGunnar Wrobel <p@rdus.de>
Wed, 9 Sep 2009 08:51:11 +0000 (10:51 +0200)
16 files changed:
framework/Kolab_FreeBusy/COPYING [new file with mode: 0644]
framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy.php [new file with mode: 0644]
framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Access.php [new file with mode: 0644]
framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php [new file with mode: 0644]
framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Imap.php [new file with mode: 0644]
framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Report.php [new file with mode: 0644]
framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/View.php [new file with mode: 0644]
framework/Kolab_FreeBusy/lib/Horde/Kolab/Test/FreeBusy.php [new file with mode: 0644]
framework/Kolab_FreeBusy/package.xml [new file with mode: 0644]
framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/AllTests.php [new file with mode: 0644]
framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/FreeBusyScenarioTest.php [new file with mode: 0644]
framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/FreeBusyTest.php [new file with mode: 0644]
framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/config.php [new file with mode: 0644]
framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/freebusy.php [new file with mode: 0644]
framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/pfb.php [new file with mode: 0644]
framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/regenerate.php [new file with mode: 0644]

diff --git a/framework/Kolab_FreeBusy/COPYING b/framework/Kolab_FreeBusy/COPYING
new file mode 100644 (file)
index 0000000..d1c6b98
--- /dev/null
@@ -0,0 +1,504 @@
+                 GNU LESSER GENERAL PUBLIC LICENSE
+                      Version 2.1, February 1999
+
+ Copyright 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+\f
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+\f
+                 GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+\f
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+\f
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+\f
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+\f
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+\f
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+\f
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                           NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy.php b/framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy.php
new file mode 100644 (file)
index 0000000..3211274
--- /dev/null
@@ -0,0 +1,340 @@
+<?php
+/**
+ * The Kolab implementation of free/busy.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy.php,v 1.14 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * @package Kolab_FreeBusy
+ */
+
+/** PEAR for raising errors */
+require_once 'PEAR.php';
+
+/** View classes for the result */
+require_once 'Horde/Kolab/FreeBusy/View.php';
+
+/** A class that handles access restrictions */
+require_once 'Horde/Kolab/FreeBusy/Access.php';
+
+/**
+ * How to use this class
+ *
+ * require_once 'config.php';
+ *
+ * $fb = new Kolab_Freebusy();
+ *
+ * $fb->trigger();
+ *
+ * OR
+ *
+ * $fb->fetch();
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy.php,v 1.14 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @since   Horde 3.2
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @author  Thomas Arendsen Hein <thomas@intevation.de>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy {
+
+    /**
+     * Parameters provided to this class.
+     *
+     * @var array
+     */
+    var $_params;
+
+    /**
+     * Link to the cache.
+     *
+     * @var Horde_Kolab_FreeBusy_Cache
+     */
+    var $_cache;
+
+    /**
+     * Setup the cache.
+     */
+    function _initCache()
+    {
+        global $conf;
+
+        /* Load the cache class now */
+        require_once 'Horde/Kolab/FreeBusy/Cache.php';
+
+        /* Where is the cache data stored? */
+        if (!empty($conf['fb']['cache_dir'])) {
+            $cache_dir = $conf['fb']['cache_dir'];
+        } else {
+            if (class_exists('Horde')) {
+                $cache_dir = Horde::getTempDir();
+            } else {
+                $cache_dir = '/tmp';
+            }
+        }
+
+        $this->_cache = new Horde_Kolab_FreeBusy_Cache($cache_dir);
+    }
+
+    /**
+     * Trigger regeneration of free/busy data in a calender.
+     */
+    function &trigger()
+    {
+        global $conf;
+
+        /* Get the folder name */
+        $req_folder = Horde_Util::getFormData('folder', '');
+
+        Horde::logMessage(sprintf("Starting generation of partial free/busy data for folder %s",
+                                  $req_folder), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        /* Validate folder access */
+        $access = new Horde_Kolab_FreeBusy_Access();
+        $result = $access->parseFolder($req_folder);
+        if (is_a($result, 'PEAR_Error')) {
+            $error = array('type' => FREEBUSY_ERROR_NOTFOUND,
+                           'error' => $result);
+            $view = new Horde_Kolab_FreeBusy_View_error($error);
+            return $view;
+        }
+
+        Horde::logMessage(sprintf("Partial free/busy data of owner %s on server %s requested by user %s.",
+                                  $access->owner, $access->freebusyserver, $access->user),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        /* Get the cache request variables */
+        $req_cache    = Horde_Util::getFormData('cache', false);
+        $req_extended = Horde_Util::getFormData('extended', false);
+
+        /* Try to fetch the data if it is stored on a remote server */
+        $result = $access->fetchRemote(true, $req_extended);
+        if (is_a($result, 'PEAR_Error')) {
+            $error = array('type' => FREEBUSY_ERROR_UNAUTHORIZED,
+                           'error' => $result);
+            $view = new Horde_Kolab_FreeBusy_View_error($error);
+            return $view;
+        }
+
+        $this->_initCache();
+
+        if (!$req_cache) {
+            /* User wants to regenerate the cache */
+
+            /* Here we really need an authenticated IMAP user */
+            $result = $access->authenticated();
+            if (is_a($result, 'PEAR_Error')) {
+                $error = array('type' => FREEBUSY_ERROR_UNAUTHORIZED,
+                               'error' => $result);
+                $view = new Horde_Kolab_FreeBusy_View_error($error);
+                return $view;
+            }
+
+            if (empty($access->owner)) {
+                $message = sprintf(_("No such account %s!"),
+                                   htmlentities($access->req_owner));
+                $error = array('type' => FREEBUSY_ERROR_NOTFOUND,
+                               'error' => PEAR::raiseError($message));
+                $view = new Horde_Kolab_FreeBusy_View_error($error);
+                return $view;
+            }
+
+            /* Update the cache */
+            $result = $this->_cache->store($access);
+            if (is_a($result, 'PEAR_Error')) {
+                $error = array('type' => FREEBUSY_ERROR_NOTFOUND,
+                               'error' => $result);
+                $view = new Horde_Kolab_FreeBusy_View_error($error);
+                return $view;
+            }
+        }
+
+        /* Load the cache data */
+        $vfb = $this->_cache->loadPartial($access, $req_extended);
+        if (is_a($vfb, 'PEAR_Error')) {
+            $error = array('type' => FREEBUSY_ERROR_NOTFOUND,
+                           'error' => $vfb);
+            $view = new Horde_Kolab_FreeBusy_View_error($error);
+            return $view;
+        }
+
+        Horde::logMessage("Delivering partial free/busy data.", __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        /* Generate the renderer */
+        $data = array('fb' => $vfb, 'name' => $access->owner . '.ifb');
+        $view = new Horde_Kolab_FreeBusy_View_vfb($data);
+
+        /* Finish up */
+        Horde::logMessage("Free/busy generation complete.", __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        return $view;
+    }
+
+    /**
+     * Fetch the free/busy data for a user.
+     */
+    function &fetch()
+    {
+        global $conf;
+
+        /* Get the user requsted */
+        $req_owner = Horde_Util::getFormData('uid');
+
+        Horde::logMessage(sprintf("Starting generation of free/busy data for user %s",
+                                  $req_owner), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        /* Validate folder access */
+        $access = new Horde_Kolab_FreeBusy_Access();
+        $result = $access->parseOwner($req_owner);
+        if (is_a($result, 'PEAR_Error')) {
+            $error = array('type' => FREEBUSY_ERROR_NOTFOUND, 'error' => $result);
+            $view = new Horde_Kolab_FreeBusy_View_error($error);
+            return $view;
+        }
+
+        Horde::logMessage(sprintf("Free/busy data of owner %s on server %s requested by user %s.",
+                                  $access->owner, $access->freebusyserver, $access->user),
+                          __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $req_extended = Horde_Util::getFormData('extended', false);
+
+        /* Try to fetch the data if it is stored on a remote server */
+        $result = $access->fetchRemote(false, $req_extended);
+        if (is_a($result, 'PEAR_Error')) {
+            $error = array('type' => FREEBUSY_ERROR_UNAUTHORIZED, 'error' => $result);
+            $view = new Horde_Kolab_FreeBusy_View_error($error);
+            return $view;
+        }
+
+        $this->_initCache();
+
+        $result = $this->_cache->load($access, $req_extended);
+        if (is_a($result, 'PEAR_Error')) {
+            $error = array('type' => FREEBUSY_ERROR_NOTFOUND, 'error' => $result);
+            $view = new Horde_Kolab_FreeBusy_View_error($error);
+            return $view;
+        }
+
+        Horde::logMessage("Delivering complete free/busy data.", __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        /* Generate the renderer */
+        $data = array('fb' => $result, 'name' => $access->owner . '.vfb');
+        $view = new Horde_Kolab_FreeBusy_View_vfb($data);
+
+        /* Finish up */
+        Horde::logMessage("Free/busy generation complete.", __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        return $view;
+    }
+
+    /**
+     * Regenerate the free/busy cache.
+     */
+    function &regenerate($reporter)
+    {
+        $access = new Horde_Kolab_FreeBusy_Access();
+        $result = $access->authenticated();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result->getMessage();
+        }
+
+        /* Load the required Kolab libraries */
+        require_once "Horde/Kolab/Storage/List.php";
+
+        $list = &Kolab_List::singleton();
+        $calendars = $list->getByType('event');
+        if (is_a($calendars, 'PEAR_Error')) {
+            return $calendars->getMessage();
+        }
+
+        $this->_initCache();
+
+        $lines = array();
+
+        foreach ($calendars as $calendar) {
+            /**
+             * We are using imap folders for our calendar list but
+             * the library expects us to follow the trigger format
+             * used by pfb.php
+             */
+            $req_domain = explode('@', $calendar->name);
+            if (isset($req_domain[1])) {
+                $domain = $req_domain[1];
+            } else {
+                $domain = null;
+            }
+            $req_folder = explode('/', $req_domain[0]);
+            if ($req_folder[0] == 'user') {
+                unset($req_folder[0]);
+                $owner = $req_folder[1];
+                unset($req_folder[1]);
+            } else if ($req_folder[0] == 'INBOX') {
+                $owner = $access->user;
+                unset($req_folder[0]);
+            }
+
+            $trigger = $owner . ($domain ? '@' . $domain : '') . '/' . join('/', $req_folder);
+            $trigger = Horde_String::convertCharset($trigger, 'UTF7-IMAP', 'UTF-8');
+
+            /* Validate folder access */
+            $result = $access->parseFolder($trigger);
+            if (is_a($result, 'PEAR_Error')) {
+                $reporter->failure($calendar->name, $result->getMessage());
+                continue;
+            }
+
+            /* Hack for allowing manager access */
+            if ($access->user == 'manager') {
+                $imapc = &Horde_Kolab_IMAP::singleton($GLOBALS['conf']['kolab']['imap']['server'],
+                                                      $GLOBALS['conf']['kolab']['imap']['port']);
+                $result = $imapc->connect($access->user, Horde_Auth::getCredential('password'));
+                if (is_a($result, 'PEAR_Error')) {
+                    $reporter->failure($calendar->name, $result->getMessage());
+                    continue;
+                }
+                $acl = $imapc->getACL($calendar->name);
+                if (is_a($acl, 'PEAR_Error')) {
+                    $reporter->failure($calendar->name, $result->getMessage());
+                    continue;
+                }
+                $oldacl = '';
+                if (isset($acl['manager'])) {
+                    $oldacl = $acl['manager'];
+                }
+                $result = $imapc->setACL($calendar->name, 'manager', 'lrs');
+                if (is_a($result, 'PEAR_Error')) {
+                    $reporter->failure($calendar->name, $result->getMessage());
+                    continue;
+                }
+            }
+
+            /* Update the cache */
+            $result = $this->_cache->store($access);
+            if (is_a($result, 'PEAR_Error')) {
+                $reporter->failure($calendar->name, $result->getMessage());
+                continue;
+            }
+
+            /* Revert the acl  */
+            if ($access->user == 'manager' && $oldacl) {
+                $result = $imapc->setACL($calendar->name, 'manager', $oldacl);
+                if (is_a($result, 'PEAR_Error')) {
+                    $reporter->failure($calendar->name, $result->getMessage());
+                    continue;
+                }
+            }
+
+            $reporter->success($calendar->name);
+
+        }
+        return $lines;
+    }
+}
+
+
diff --git a/framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Access.php b/framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Access.php
new file mode 100644 (file)
index 0000000..0bb5087
--- /dev/null
@@ -0,0 +1,442 @@
+<?php
+/**
+ * The Horde_Kolab_FreeBusy_Access:: class provides functionality to check
+ * free/busy access rights for the specified folder.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Access.php,v 1.23 2009/07/08 18:39:07 slusarz Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_Access {
+
+    /**
+     * The user calling the script.
+     *
+     * @var string
+     */
+    var $user;
+
+    /**
+     * Did the above combination authenticate?
+     *
+     * @var string
+     */
+    var $_authenticated = false;
+
+    /**
+     * The object representing the user calling the script.
+     *
+     * @var string
+     */
+    var $user_object;
+
+    /**
+     * The requested owner.
+     *
+     * @var string
+     */
+    var $owner;
+
+    /**
+     * The object representing the folder owner.
+     *
+     * @var string
+     */
+    var $owner_object;
+
+    /**
+     * The object representing the server configuration.
+     *
+     * @var string
+     */
+    var $server_object;
+
+    /**
+     * The folder we try to access.
+     *
+     * @var string
+     */
+    var $folder;
+
+    /**
+     * The IMAP path of folder we try to access.
+     *
+     * @var string
+     */
+    var $imap_folder;
+
+    /**
+     * The common name (CN) of the owner.
+     *
+     * @var string
+     */
+    var $cn = '';
+
+    /**
+     * The free/busy server for the folder owner.
+     *
+     * @var string
+     */
+    var $freebusyserver;
+
+    /**
+     * Constructor.
+     *
+     * @param array       $params        Any additional options
+     */
+    function Horde_Kolab_FreeBusy_Access()
+    {
+        $this->_parseUser();
+    }
+
+    /**
+     * Parse the requested folder for the owner of that folder.
+     *
+     * @param string $req_folder The folder requested.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function parseFolder($req_folder = '')
+    {
+        /* Handle the owner/folder name and make sure the owner part is in lower case */
+        $req_folder = Horde_String::convertCharset($req_folder, 'UTF-8', 'UTF7-IMAP');
+        $folder = explode('/', $req_folder);
+        if (count($folder) < 2) {
+            return PEAR::raiseError(sprintf(_("No such folder %s"), $req_folder));
+        }
+
+        $folder[0] = strtolower($folder[0]);
+        $req_folder = implode('/', $folder);
+        $this->owner = $folder[0];
+        unset($folder[0]);
+        $this->folder = join('/', $folder);
+
+        $result = $this->_process();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        return true;
+    }
+
+    /**
+     * Parse the owner value.
+     *
+     * @param string $owner The owner that should be processed.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function parseOwner($owner = '')
+    {
+        $this->owner = $owner;
+
+        $result = $this->_process();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        return true;
+    }
+
+    /**
+     * Fetch remote free/busy user if the current user is not local or
+     * redirect to the other server if configured this way.
+     *
+     * @param boolean $trigger Have we been called for triggering?
+     * @param boolean $extended Should the extended information been delivered?
+     */
+    function fetchRemote($trigger = false, $extended = false)
+    {
+        global $conf;
+
+        if (!empty($conf['kolab']['freebusy']['server'])) {
+            $server = $conf['kolab']['freebusy']['server'];
+        } else {
+            $server = 'https://localhost/freebusy';
+        }
+        if (!empty($conf['fb']['redirect'])) {
+            $do_redirect = $conf['fb']['redirect'];
+        } else {
+            $do_redirect = false;
+        }
+
+        if ($trigger) {
+            $path = sprintf('/trigger/%s/%s.' . ($extended)?'pxfb':'pfb',
+                            urlencode($this->owner), urlencode($this->imap_folder));
+        } else {
+            $path = sprintf('/%s.' . ($extended)?'xfb':'ifb', urlencode($this->owner));
+        }
+
+        /* Check if we are on the right server and redirect if appropriate */
+        if ($this->freebusyserver && $this->freebusyserver != $server) {
+            $redirect = $this->freebusyserver . $path;
+            Horde::logMessage(sprintf("URL %s indicates remote free/busy server since we only offer %s. Redirecting.", 
+                                      $this->freebusyserver, $server), __FILE__,
+                              __LINE__, PEAR_LOG_ERR);
+            if ($do_redirect) {
+                header("Location: $redirect");
+            } else {
+                header("X-Redirect-To: $redirect");
+                $redirect = 'https://' . urlencode($this->user) . ':' . urlencode(Horde_Auth::getCredential('password'))
+                    . '@' . $this->freebusyserver . $path;
+                if (!@readfile($redirect)) {
+                    $message = sprintf(_("Unable to read free/busy information from %s"), 
+                                       'https://' . urlencode($this->user) . ':XXX'
+                                       . '@' . $this->freebusyserver . $_SERVER['REQUEST_URI']);
+                    return PEAR::raiseError($message);
+                }
+            }
+            exit;
+        }
+    }
+
+    /**
+     * Check if we are in an authenticated situation.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function authenticated()
+    {
+        global $conf;
+
+        if (empty($this->user)) {
+            header('WWW-Authenticate: Basic realm="Kolab Freebusy"');
+            return PEAR::raiseError(_("Please authenticate!"));
+        }
+
+        if (!$this->_authenticated) {
+            return PEAR::raiseError(sprintf(_("Invalid authentication for user %s!"), 
+                                            $this->user));
+        }
+        return true;
+    }
+
+    /**
+     * Parse the current user accessing the page and try to
+     * authenticate the user.
+     */
+    function _parseUser()
+    {
+        global $conf;
+
+        $this->user = Horde_Auth::getAuth();
+
+        if (empty($this->user)) {
+            $this->user = isset($_SERVER['PHP_AUTH_USER'])?$_SERVER['PHP_AUTH_USER']:false;
+            $pass = isset($_SERVER['PHP_AUTH_PW'])?$_SERVER['PHP_AUTH_PW']:false;
+        } else {
+            $this->_authenticated = true;
+            return;
+        }
+
+        // This part allows you to use the PHP scripts with CGI rather than as
+        // an apache module. This will of course slow down things but on the
+        // other hand it allows you to reduce the memory footprint of the 
+        // apache server. The default is to use PHP as a module and the CGI 
+        // version requires specific Apache configuration.
+        //
+        // The line you need to add to your configuration of the /freebusy 
+        // location of your server looks like this:
+        //
+        //    RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization}]
+        //
+        // The complete section will probably look like this then:
+        //
+        //  <IfModule mod_rewrite.c>
+        //    RewriteEngine On
+        //    # FreeBusy list handling
+        //    RewriteBase /freebusy
+        //    RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization}]
+        //    RewriteRule ^([^/]+)\.ifb       freebusy.php?uid=$1                  [L]
+        //    RewriteRule ^([^/]+)\.vfb       freebusy.php?uid=$1                  [L]
+        //    RewriteRule ^([^/]+)\.xfb       freebusy.php?uid=$1&extended=1        [L]
+        //    RewriteRule ^trigger/(.+)\.pfb  pfb.php?folder=$1&cache=0             [L]
+        //    RewriteRule ^(.+)\.pfb          pfb.php?folder=$1&cache=1             [L]
+        //    RewriteRule ^(.+)\.pxfb         pfb.php?folder=$1&cache=1&extended=1  [L]
+        //  </IfModule>
+        if (empty($this->user) && isset($_ENV['REDIRECT_REDIRECT_REMOTE_USER'])) {
+            $a = base64_decode(substr($_ENV['REDIRECT_REDIRECT_REMOTE_USER'], 6)) ;
+            if ((strlen($a) != 0) && (strcasecmp($a, ':') == 0)) {
+                list($this->user, $pass) = explode(':', $a, 2);
+            }
+        }
+
+        if (!empty($this->user)) {
+            /* Load the authentication libraries */
+            $auth = Horde_Auth::singleton(isset($conf['auth']['driver'])?$conf['auth']['driver']:'kolab');
+            if (!$this->_authenticated) {
+                $this->_authenticated = $auth->authenticate($this->user, array('password' => $pass), false);
+            }
+            if ($this->_authenticated) {
+                @session_start();
+                $_SESSION['__auth'] = array(
+                    'authenticated' => true,
+                    'userId' => $this->user,
+                    'timestamp' => time(),
+                    'remote_addr' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null,
+                );
+                Horde_Auth::setCredential('password', $pass);
+            }
+        }
+    }
+
+    /**
+     * Process both the user accessing the page as well as the
+     * owner of the requested free/busy information.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function _process() 
+    {
+        global $conf;
+
+        require_once 'Horde/Kolab/Server.php';
+
+        if (isset($conf['kolab']['ldap']['phpdn'])) {
+            $params = array(
+                'uid' => $conf['kolab']['ldap']['phpdn'],
+                'pass' => $conf['kolab']['ldap']['phppw'],
+            );
+        } else {
+            $params = array(
+                'user' => Horde_Auth::getAuth(),
+                'pass' => Horde_Auth::getCredential('password'),
+            );
+        }
+
+        /* Connect to the Kolab user database */
+        $db = &Horde_Kolab_Server::singleton($params);
+        // TODO: Remove once Kolab_Server has been fixed to always return the base dn
+        $db->fetch();
+
+        /* Retrieve the server configuration */
+        try {
+            $server = $db->fetch(sprintf('k=kolab,%s',
+                                         $db->getBaseUid()),
+                                 'Horde_Kolab_Server_Object_Kolab_Server');
+            $this->server_object = $server;
+        } catch (Horde_Kolab_Server_Exception $e) {
+            Horde::logMessage(sprintf("Failed fetching the k=kolab configuration object. Error was: %s", 
+                                      $e->getMessage()),
+                              __FILE__, __LINE__, PEAR_LOG_ERR);
+            $this->server_object = null;
+        }
+
+        /* Fetch the user calling us */
+        $udn = $db->uidForIdOrMail($this->user);
+        if (is_a($udn, 'PEAR_Error')) {
+            return $udn;
+        }
+        if ($udn) {
+            $user = $db->fetch($udn, 'Horde_Kolab_Server_Object_Kolab_User');
+            if (is_a($user, 'PEAR_Error')) {
+                return $user;
+            }
+            $this->user_object = $user;
+        }
+
+        if ($this->user_object && $this->user_object->exists()) {
+            $mail = $this->user_object->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_MAIL);
+            if (is_a($mail, 'PEAR_Error')) {
+                return $mail;
+            }
+            if ($mail) {
+                $this->user = $mail;
+            }
+        }
+
+        /* Fetch the owner of the free/busy data */
+        $odn = $db->uidForIdOrMailOrAlias($this->owner);
+        if (is_a($odn, 'PEAR_Error')) {
+            return $odn;
+        }
+        if (!$odn) {
+            $idx = strpos($this->user, '@');
+            if($idx !== false) {
+                $domain = substr($this->user, $idx+1);
+                Horde::logMessage(sprintf("Trying to append %s to %s", 
+                                          $domain, $this->owner),
+                                  __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                $odn = $odn = $db->uidForIdOrMail($this->owner . '@' . $domain);
+            }
+        }
+
+        if ($odn) {
+            $owner = $db->fetch($odn, 'Horde_Kolab_Server_Object_Kolab_User');
+            if (is_a($owner, 'PEAR_Error')) {
+                return $owner;
+            }
+            $this->owner_object = &$owner;
+        }
+
+        if (!empty($this->owner_object)) {
+            if ($this->owner_object->exists()) {
+                $this->owner = $this->owner_object->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_MAIL);
+
+                $freebusyserver = $this->owner_object->getServer('freebusy');
+                if (!is_a($freebusyserver, 'PEAR_Error')) {
+                    $this->freebusyserver = $freebusyserver;
+                }
+            }
+        } else {
+            return PEAR::raiseError(_("Unable to determine owner of the free/busy data!"));
+        }
+
+        /* Mangle the folder request into an IMAP folder name */
+        $this->imap_folder = $this->_getImapFolder();
+
+        return true;
+    }
+
+    /**
+     * Calculate the correct IMAP folder name to access based on the
+     * combination of user and owner.
+     *
+     * @return string The IMAP folder we should access.
+     */
+    function _getImapFolder() 
+    {
+        $userdom = false;
+        $ownerdom = false;
+        if (ereg( '(.*)@(.*)', $this->user, $regs)) {
+            // Regular user
+            $user = $regs[1];
+            $userdom  = $regs[2];
+        } else {
+            $user = $this->user;
+        }
+
+        if(ereg( '(.*)@(.*)', $this->owner, $regs)) {
+            // Regular owner
+            $owner = $regs[1];
+            $ownerdom = $regs[2];
+        } else {
+            $owner = $this->owner;
+        }
+
+        $fldrcomp = array();
+        if ($user == $owner) {
+            $fldrcomp[] = 'INBOX';
+        } else {
+            $fldrcomp[] = 'user';
+            $fldrcomp[] = $owner;
+        }
+
+        if (!empty($this->folder)) {
+            $fldrcomp[] = $this->folder;
+        }
+
+        $folder = join('/', $fldrcomp);
+        if ($ownerdom && !$userdom) {
+            $folder .= '@' . $ownerdom;
+        }
+        return $folder;
+    }
+
+}
+
diff --git a/framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php b/framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php
new file mode 100644 (file)
index 0000000..d52d38f
--- /dev/null
@@ -0,0 +1,1491 @@
+<?php
+/**
+ * Caching for the Kolab free/busy data.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php,v 1.27 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * @package Kolab_FreeBusy
+ */
+
+/** We require the iCalendar library to build the free/busy list */
+require_once 'Horde/iCalendar.php';
+require_once 'Horde/iCalendar/vfreebusy.php';
+
+/**
+ * The Horde_Kolab_FreeBusy_Cache:: class provides functionality to store
+ * prepared free/busy data for quick retrieval.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php,v 1.27 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_Cache {
+
+    /**
+     * The directory that should be used for caching.
+     *
+     * @var string
+     */
+    var $_cache_dir;
+
+    /**
+     * Constructor.
+     *
+     * @param string  $cache_dir  The cache directory we should use.
+     */
+    function Horde_Kolab_FreeBusy_Cache($cache_dir)
+    {
+        $this->_cache_dir = $cache_dir;
+    }
+
+    /**
+     * Update the cache information for a calendar.
+     *
+     * @param Horde_Kolab_FreeBusy_Access $access The object holding the
+     *                                      relevant access
+     *                                      parameters.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function store($access)
+    {
+        global $conf;
+
+        /* Now we really need the free/busy library */
+        require_once 'Horde/Kolab/FreeBusy/Imap.php';
+
+        $fb = new Horde_Kolab_FreeBusy_Imap();
+
+        $result = $fb->connect($access->imap_folder);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $fbpast = $fbfuture = null;
+        try {
+            if (!empty($access->server_object)) {
+                $result = $access->server_object->get(Horde_Kolab_Server_Object_Kolab_Server::ATTRIBUTE_FBPAST);
+                if (!is_a($result, 'PEAR_Error')) {
+                    $fbpast = $result;
+                }
+            }
+        } catch (Horde_Kolab_Server_Exception $e) {
+            Horde::logMessage(sprintf("Failed fetching the k=kolab configuration object. Error was: %s",
+                                      $e->getMessage()),
+                              __FILE__, __LINE__, PEAR_LOG_ERR);
+            if (isset($conf['kolab']['freebusy']['past'])) {
+                $fbpast = $conf['kolab']['freebusy']['past'];
+            } else {
+                $fbpast = 10;
+            }
+        }
+
+        if (!empty($access->owner_object)) {
+            $result = $access->owner_object->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_FBFUTURE);
+            if (!is_a($result, 'PEAR_Error')) {
+                $fbfuture = $result;
+            }
+        }
+
+        $vCal = $fb->generate(null, null,
+                              !empty($fbpast)?$fbpast:0,
+                              !empty($fbfuture)?$fbfuture:60,
+                              $access->owner,
+                              $access->owner_object->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_CN));
+        if (is_a($vCal, 'PEAR_Error')) {
+            $vCal;
+        }
+
+        $fbfilename = $this->_getFilename($access->folder, $access->owner);
+
+        $c_pvcal = new Horde_Kolab_FreeBusy_Cache_File_pvcal($this->_cache_dir, $fbfilename);
+
+        if (!empty($conf['fb']['use_acls'])) {
+            $c_acl   = new Horde_Kolab_FreeBusy_Cache_File_acl($this->_cache_dir, $fbfilename);
+            $c_xacl  = new Horde_Kolab_FreeBusy_Cache_File_xacl($this->_cache_dir, $fbfilename);
+        }
+
+        /* missing data means delete the cache files */
+        if (empty($vCal)) {
+            Horde::logMessage(sprintf("No events. Purging cache %s.",
+                                      $fbfilename),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+            $result = $c_pvcal->purge();
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            if (!empty($conf['fb']['use_acls'])) {
+            $result = $c_acl->purge();
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+            $result = $c_xacl->purge();
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+            }
+        } else {
+            $result = $c_pvcal->storePVcal($vCal);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+
+            $relevance = $fb->getRelevance();
+            if (is_a($relevance, 'PEAR_Error')) {
+                return $relevance;
+            }
+
+            if (!empty($conf['fb']['use_acls'])) {
+                $acl = $fb->getACL();
+                if (is_a($acl, 'PEAR_Error')) {
+                    return $acl;
+                }
+
+                /**
+                 * Only store the acl information if the current user
+                 * has admin rights on the folder and can actually
+                 * retrieve the full ACL information.
+                 *
+                 * A folder that does not have admin rights for a user
+                 * will not be considered relvant for that user unless
+                 * it has been triggered by the folder owner before.
+                 */
+                $append = false;
+                if (isset($acl[$access->user])) {
+                    $myacl = $acl[$access->user];
+                    if (strpos($myacl, 'a') !== false) {
+                        $append = true;
+                    }
+                }
+
+                $result = $c_acl->storeACL($acl, $relevance, $append);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+
+                $xacl = $fb->getExtendedACL();
+                if (is_a($xacl, 'PEAR_Error')) {
+                    return $xacl;
+                }
+
+                $result = $c_xacl->storeXACL($xacl, $acl);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            } else {
+                $acl = null;
+            }
+
+            Horde::logMessage(sprintf("Horde_Kolab_FreeBusy_Cache::store(file=%s, relevance=%s, acl=%s, xacl=%s)",
+                                      $fbfilename, $relevance, $acl, $xacl),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        }
+        return true;
+    }
+
+    /**
+     * Load partial free/busy data.
+     *
+     * @param Horde_Kolab_FreeBusy_Access $access   The object holding the
+     *                                        relevant access
+     *                                        parameters.
+     * @param boolean               $extended Should the data hold the extended
+     *                                        free/busy information?
+     *
+     * @return Horde_iCalendar|PEAR_Error The free/busy data of a
+     *                                    single calendar.
+     */
+    function &loadPartial(&$access, $extended)
+    {
+        global $conf;
+
+        $file = $this->_getFilename($access->folder, $access->owner);
+
+        if (!empty($conf['fb']['use_acls'])) {
+            $aclcache = &Horde_Kolab_FreeBusy_Cache_DB_acl::singleton('acl',
+                                                                      $this->_cache_dir);
+            if ($extended) {
+                $extended = $this->_allowExtended($file, $access);
+            }
+        }
+
+        $c_pvcal = new Horde_Kolab_FreeBusy_Cache_File_pvcal($this->_cache_dir, $file);
+        $pvCal = $c_pvcal->loadPVcal($extended);
+        if (is_a($pvCal, 'PEAR_Error')) {
+            return $pvCal;
+        }
+        return $pvCal;
+    }
+
+    /**
+     * Load the complete free/busy data of a user.
+     *
+     * @param Horde_Kolab_FreeBusy_Access $access   The object holding the
+     *                                        relevant access
+     *                                        parameters.
+     * @param boolean               $extended Should the data hold the extended
+     *                                        free/busy information?
+     *
+     * @return Horde_iCalendar|PEAR_Error The free/busy data for a user.
+     */
+    function &load(&$access, $extended)
+    {
+        global $conf;
+
+        /* Which files will we access? */
+        if (!empty($conf['fb']['use_acls'])) {
+            $aclcache = &Horde_Kolab_FreeBusy_Cache_DB_acl::singleton('acl', $this->_cache_dir);
+            $files = $aclcache->get($access->owner);
+            if (is_a($files, 'PEAR_Error')) {
+                return $files;
+            }
+        } else {
+            $file_uid = str_replace("\0", '', str_replace(".", "^", $access->owner));
+            $files = array();
+            $this->findAll_readdir($file_uid, $conf['fb']['cache_dir'].'/'.$file_uid, $files);
+        }
+
+        $owner = $access->owner;
+        if (ereg('(.*)@(.*)', $owner, $regs)) {
+            $owner = $regs[2] . '/' . $regs[1];
+        }
+        $user = $access->user;
+        if (ereg('(.*)@(.*)', $user, $regs)) {
+            $user = $regs[2] . '/' . $regs[1];
+        }
+        $c_file = str_replace("\0", '', str_replace('.', '^', $user . '/' . $owner));
+
+        $c_vcal = new Horde_Kolab_FreeBusy_Cache_File_vcal($this->_cache_dir,
+                                $c_file, $extended);
+
+        /* If the current vCal cache did not expire, we can deliver it */
+        if (!$c_vcal->expired($files)) {
+            return $c_vcal->loadVcal();
+        }
+
+        // Create the new iCalendar.
+        $vCal = new Horde_iCalendar();
+        $vCal->setAttribute('PRODID', '-//kolab.org//NONSGML Kolab Server 2//EN');
+        $vCal->setAttribute('METHOD', 'PUBLISH');
+
+        // Create new vFreebusy.
+        $vFb = &Horde_iCalendar::newComponent('vfreebusy', $vCal);
+        $params = array();
+
+        $cn = $access->owner_object->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_CN);
+        if (!empty($cn) || is_a($cn, 'PEAR_Error')) {
+            $params['cn'] = $access->owner_object->get(Horde_Kolab_Server_Object_Kolab_User::ATTRIBUTE_CN);
+        }
+        $vFb->setAttribute('ORGANIZER', 'MAILTO:' . $access->owner, $params);
+
+        $vFb->setAttribute('DTSTAMP', time());
+        if (isset($_SERVER['SERVER_NAME'])) {
+            $host = $_SERVER['SERVER_NAME'];
+        } else {
+            $host = 'localhost';
+        }
+        if (isset($_SERVER['REQUEST_URI'])) {
+            $uri = $_SERVER['REQUEST_URI'];
+        } else {
+            $uri = '/';
+        }
+        $vFb->setAttribute('URL', 'http://' . $host . $uri);
+
+        $mtimes = array();
+        foreach ($files as $file) {
+            if ($extended && !empty($conf['fb']['use_acls'])) {
+                $extended_pvc = $this->_allowExtended($file, $access);
+            } else {
+                $extended_pvc = $extended;
+            }
+            $c_pvcal = new Horde_Kolab_FreeBusy_Cache_File_pvcal($this->_cache_dir, $file);
+            $pvCal = $c_pvcal->loadPVcal($extended_pvc);
+            if (is_a($pvCal, 'PEAR_Error')) {
+                Horde::logMessage(sprintf("Ignoring partial free/busy file %s: %s)",
+                                          $file, $pvCal->getMessage()),
+                                  __FILE__, __LINE__, PEAR_LOG_INFO);
+                continue;
+            }
+            $pvFb = &$pvCal->findComponent('vfreebusy');
+            if( !$pvFb ) {
+                Horde::logMessage(sprintf("Could not find free/busy info in file %s.)",
+                                          $file), __FILE__, __LINE__, PEAR_LOG_INFO);
+                continue;
+            }
+            if ($ets = $pvFb->getAttributeDefault('DTEND', false) !== false) {
+                // PENDING(steffen): Make value configurable
+                if ($ets < time()) {
+                    Horde::logMessage(sprintf("Free/busy info in file %s is too old.)",
+                                              $file), __FILE__, __LINE__, PEAR_LOG_INFO);
+                    $c_pvcal->purge();
+                    continue;
+                }
+            }
+            $vFb->merge($pvFb);
+
+            /* Store last modification time */
+            $mtimes[$file] = array($c_pvcal->getFile(), $c_pvcal->getMtime());
+        }
+
+        if (!empty($conf['fb']['remote_servers'])) {
+            $remote_vfb = $this->_fetchRemote($conf['fb']['remote_servers'],
+                                              $access);
+            if (is_a($remote_vfb, 'PEAR_Error')) {
+                Horde::logMessage(sprintf("Ignoring remote free/busy files: %s)",
+                                          $remote_vfb->getMessage()),
+                                  __FILE__, __LINE__, PEAR_LOG_INFO);
+            } else {
+                $vFb->merge($remote_vfb);
+            }
+        }
+
+        if (!(boolean)$vFb->getBusyPeriods()) {
+            /* No busy periods in fb list. We have to add a
+             * dummy one to be standards compliant
+             */
+            $vFb->setAttribute('COMMENT', 'This is a dummy vfreebusy that indicates an empty calendar');
+            $vFb->addBusyPeriod('BUSY', 0,0, null);
+        }
+
+        $vCal->addComponent($vFb);
+
+        $c_vcal->storeVcal($vCal, $mtimes);
+
+        return $vCal;
+    }
+
+    /**
+     * Is extended access to the given file allowed?
+     *
+     * @param string                $file     Name of the cache file.
+     * @param Horde_Kolab_FreeBusy_Access $access   The object holding the
+     *                                        relevant access
+     *                                        parameters.
+     *
+     * @return boolean|PEAR_Error True if extended access is allowed.
+     */
+    function _allowExtended($file, &$access)
+    {
+        if (!isset($access->user_object)) {
+            Horde::logMessage(sprintf("Extended attributes on folder %s disallowed for unknown user.",
+                                      $access->folder, $access->user), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            return false;
+        }
+
+        $xaclcache = &Horde_Kolab_FreeBusy_Cache_DB_xacl::singleton('xacl', $this->_cache_dir);
+
+        /* Check if the calling user has access to the extended information of
+         * the folder we are about to integrate into the free/busy data.
+         */
+        $groups = $access->user_object->getGroupAddresses();
+        if (is_a($groups, 'PEAR_Error')) {
+            return $groups;
+        }
+
+        $groups[] = $access->user;
+        foreach ($groups as $id) {
+            if ($xaclcache->has($file, $id)) {
+                return true;
+            }
+        }
+        Horde::logMessage(sprintf("Extended attributes on folder %s disallowed for user %s.",
+                                  $access->folder, $access->user), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+        return false;
+    }
+
+    /**
+     * Get a cache file name depending on the owner of the free/busy
+     * data.
+     *
+     * @param string  $folder  Name of the calendar folder.
+     * @param string  $owner   Owner of the calendar folder.
+     *
+     * @return string Name of the correspoding cache file.
+     */
+    function _getFilename($folder, $owner)
+    {
+        if (ereg('(.*)@(.*)', $owner, $regs)) {
+            $owner = $regs[2] . '/' . $regs[1];
+        }
+
+        return str_replace("\0", '', str_replace('.', '^', $owner . '/' . $folder));
+    }
+
+    /**
+     * Retrieve external free/busy data.
+     *
+     * @param array                 $servers  The remote servers to query
+     * @param Horde_Kolab_FreeBusy_Access $access   The object holding the
+     *                                        relevant access
+     *                                        parameters.
+     *
+     * @return Horde_iCalender The remote free/busy information.
+     */
+    function &_fetchRemote($servers, $access)
+    {
+        $vFb = null;
+
+        foreach ($servers as $server) {
+
+            $url = 'https://' . urlencode($access->user) . ':' . urlencode($access->pass)
+            . '@' . $server . $_SERVER['REQUEST_URI'];
+            $remote = @file_get_contents($url);
+            if (!$remote) {
+                $message = sprintf("Unable to read free/busy information from %s",
+                                   'https://' . urlencode($access->user) . ':XXX'
+                                   . '@' . $server . $_SERVER['REQUEST_URI']);
+                Horde::logMessage($message, __FILE__, __LINE__, PEAR_LOG_INFO);
+            }
+
+            $rvCal = new Horde_iCalendar();
+            $result = $rvCal->parsevCalendar($remote);
+
+            if (is_a($result, 'PEAR_Error')) {
+                $message = sprintf("Unable to parse free/busy information from %s: %s",
+                                   'https://' . urlencode($access->user) . ':XXX'
+                                   . '@' . $server . $_SERVER['REQUEST_URI'],
+                                   $result->getMessage());
+                Horde::logMessage($message, __FILE__, __LINE__, PEAR_LOG_INFO);
+            }
+
+            $rvFb = &$rvCal->findComponent('vfreebusy');
+            if (!$pvFb) {
+                $message = sprintf("Unable to find free/busy information in data from %s.",
+                                   'https://' . urlencode($access->user) . ':XXX'
+                                   . '@' . $server . $_SERVER['REQUEST_URI']);
+                Horde::logMessage($message, __FILE__, __LINE__, PEAR_LOG_INFO);
+            }
+            if ($ets = $rvFb->getAttributeDefault('DTEND', false) !== false) {
+                // PENDING(steffen): Make value configurable
+                if ($ets < time()) {
+                    $message = sprintf("free/busy information from %s is too old.",
+                                       'https://' . urlencode($access->user) . ':XXX'
+                                       . '@' . $server . $_SERVER['REQUEST_URI']);
+                    Horde::logMessage($message, __FILE__, __LINE__, PEAR_LOG_INFO);
+                }
+            }
+            if (!empty($vFb)) {
+                $vFb->merge($rvFb);
+            } else {
+                $vFb = $rvFb;
+            }
+        }
+        return $vFb;
+    }
+
+    function findAll_readdir($uid, $dirname, &$lst) {
+        if ($dir = @opendir($dirname)) {
+            while (($file = readdir($dir)) !== false) {
+                if ($file == "." || $file == "..")
+                    continue;
+
+                $full_path = $dirname."/".$file;
+
+                if (is_file($full_path) && preg_match("/(.*)\.x?pvc$/", $file, $matches))
+                    $lst[] = $uid."/".$matches[1];
+                else if(is_dir($full_path))
+                    $this->findAll_readdir($uid."/".$file, $full_path, $lst);
+            }
+            closedir($dir);
+        }
+    }
+};
+
+/**
+ * A berkeley db based cache for free/busy data.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php,v 1.27 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_Cache_DB {
+
+    /**
+     * The directory that should be used for caching.
+     *
+     * @var string
+     */
+    var $_cache_dir;
+
+    /**
+     * The resource handle into the database.
+     *
+     * @var resource
+     */
+    var $_db = false;
+
+    /**
+     * The format of the database.
+     *
+     * @var string
+     */
+    var $_dbformat;
+
+    /**
+     * The type of this cache.
+     *
+     * @var string
+     */
+    var $_type = '';
+
+    /**
+     * The directory that should be used for caching.
+     *
+     * @var string
+     */
+    function Horde_Kolab_FreeBusy_Cache_DB($cache_dir) {
+        global $conf;
+
+        $this->_cache_dir = $cache_dir;
+
+        if (!empty($conf['fb']['dbformat'])) {
+            $this->_dbformat = $conf['fb']['dbformat'];
+        } else {
+            $this->_dbformat = 'db4';
+        }
+
+        /* make sure that a database really exists before accessing it */
+        if (!file_exists($this->_cache_dir . '/' . $this->_type . 'cache.db')) {
+            $result = $this->_open();
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+            $this->_close();
+        }
+
+    }
+
+    /**
+     * Open the database.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function _open()
+    {
+        if ($this->_db !== false) {
+            return true;
+        }
+
+        $dbfile = $this->_cache_dir . '/' . $this->_type . 'cache.db';
+        $this->_db = dba_open($dbfile, 'cd', $this->_dbformat);
+        if ($this->_db === false) {
+            return PEAR::raiseError(sprintf("Unable to open freebusy cache db %s", $dbfile));
+        }
+        return true;
+    }
+
+    /**
+     * Close the database.
+     */
+    function _close()
+    {
+        if ($this->_db !== false) {
+            dba_close($this->_db);
+        }
+        $this->_db = false;
+    }
+
+    /**
+     * Set a cache file as irrelevant for a user.
+     *
+     * @param string $filename The cache file to remove.
+     * @param string $uid      The user ID.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function _remove($filename, $uid)
+    {
+        $result = $this->_open();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (dba_exists($uid, $this->_db)) {
+            $lst = dba_fetch($uid, $this->_db);
+            $lst = explode(',', $lst);
+            $lst = array_diff($lst, array($filename));
+            $result = dba_replace($uid, join(',', $lst), $this->_db);
+            if ($result === false) {
+                $result = PEAR::raiseError(sprintf("Unable to set db value for uid %s", $uid));
+            }
+        }
+        $this->_close();
+
+        return $result;
+    }
+
+    /**
+     * Set a cache file as relevant for a user.
+     *
+     * @param string $filename The cache file to add.
+     * @param string $uid      The user ID.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function _add($filename, $uid)
+    {
+        if (empty($filename)) {
+            return true;
+        }
+
+        $result = $this->_open();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        if (dba_exists($uid, $this->_db)) {
+            $lst = dba_fetch($uid, $this->_db);
+            $lst = explode(',', $lst);
+            $lst[] = $filename;
+            $result = dba_replace($uid, join(',', array_keys(array_flip($lst))), $this->_db);
+            if ($result === false) {
+                $result = PEAR::raiseError(sprintf("Unable to set db value for uid %s", $uid));
+            }
+        } else {
+            $result = dba_insert($uid, $filename, $this->_db);
+            if ($result === false) {
+                $result = PEAR::raiseError(sprintf("Unable to set db value for uid %s", $uid));
+            }
+        }
+        $this->_close();
+
+        return $result;
+    }
+
+    /**
+     * Is the cache file relevant for the user?
+     *
+     * @param string $filename The cache file.
+     * @param string $uid      The user ID.
+     *
+     * @return boolean|PEAR_Error True if the cache file is relevant.
+     */
+    function has($filename, $uid)
+    {
+        $result = $this->_open();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $result = false;
+        if (dba_exists($uid, $this->_db)) {
+            $lst = dba_fetch($uid, $this->_db);
+            $lst = explode(',', $lst);
+            $result = in_array($filename, $lst);
+        }
+        $this->_close();
+
+        return $result;
+    }
+
+    /**
+     * Get the full list of relevant cache files for a uid.
+     *
+     * @param string $uid      The user ID.
+     *
+     * @return array|PEAR_Error The list of cache files.
+     */
+    function get($uid)
+    {
+        $result = $this->_open();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $result = array();
+        if (dba_exists($uid, $this->_db)) {
+            $lst = dba_fetch($uid, $this->_db);
+            $lst = explode(',', $lst);
+            $result = array_filter($lst, array($this, '_notEmpty'));
+        }
+        $this->_close();
+
+        return $result;
+    }
+
+    /**
+     * Check if the value is set.
+     *
+     * @param mixed $value  The value to check.
+     *
+     * @return boolean True if the value is set.
+     */
+    function _notEmpty($value)
+    {
+        return !empty($value);
+    }
+
+    /**
+     * Attempts to return a reference to a concrete FreeBusyACLCache
+     * instance. It will only create a new instance if no
+     * FreeBusyACLCache instance currently exists.
+     *
+     * This method must be invoked as:
+     *   <code>$var = &FreeBusyACLCache::singleton($cache_dir);</code>
+     *
+     * @static
+     *
+     * @param string $type       The type of the cache.
+     * @param string $cache_dir  The directory for storing the cache.
+     *
+     * @return FreeBusyACLCache The concrete FreeBusyACLCache
+     *                          reference, or false on an error.
+     */
+    function &singleton($type, $cache_dir)
+    {
+        static $cachedb = array();
+
+        $signature = $type . $cache_dir;
+
+        if (empty($cachedb[$signature])) {
+            $class = 'Horde_Kolab_FreeBusy_Cache_DB_' . $type;
+            $cachedb[$signature] = new $class($cache_dir);
+        }
+
+        return $cachedb[$signature];
+    }
+}
+
+/**
+ * A berkeley db based cache for free/busy data that holds relevant
+ * cache files based on folder ACLs.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php,v 1.27 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_Cache_DB_acl extends Horde_Kolab_FreeBusy_Cache_DB {
+
+    /**
+     * The type of this cache.
+     *
+     * @var string
+     */
+    var $_type = 'acl';
+
+    /**
+     * Store permissions on a calender folder.
+     *
+     * @param string $filename The cache file representing the calendar folder.
+     * @param array  $acl      The new ACL.
+     * @param array  $oldacl   The old ACL.
+     * @param mixed  $perm     False if all permissions should be revoked, a
+     *                         single character specifying allowed access
+     *                         otherwise.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function store($filename, $acl, $oldacl, $perm)
+    {
+        /* We remove the filename from all users listed in the old ACL first */
+        foreach ($oldacl as $user => $ac) {
+            $result = $this->_remove($filename, $user);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        /* Now add the filename for all users with the correct permissions */
+        if ($perm !== false ) {
+            foreach ($acl as $user => $ac) {
+                if (strpos($ac, $perm) !== false) {
+                    if (!empty($user)) {
+                        $result = $this->_add($filename, $user);
+                        if (is_a($result, 'PEAR_Error')) {
+                            return $result;
+                        }
+                    }
+                }
+            }
+        }
+
+        return true;
+    }
+}
+
+/**
+ * A berkeley db based cache for free/busy data that holds relevant
+ * cache files based on extended folder ACLs.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php,v 1.27 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_Cache_DB_xacl extends Horde_Kolab_FreeBusy_Cache_DB {
+
+    /**
+     * The type of this cache.
+     *
+     * @var string
+     */
+    var $_type = 'xacl';
+
+    /**
+     * Store permissions on a calender folder.
+     *
+     * @param string $filename The cache file representing the calendar folder.
+     * @param array  $xacl     The new extended ACL.
+     * @param array  $oldxacl  The old extended ACL.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function store($filename, $xacl, $oldxacl)
+    {
+        $xacl = explode(' ', $xacl);
+        $oldxacl = explode(' ', $oldxacl);
+        $both = array_intersect($xacl, $oldxacl);
+
+        /* Removed access rights */
+        foreach (array_diff($oldxacl, $both) as $uid) {
+            if (!empty($uid)) {
+                $result = $this->_remove($filename, $uid);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+        }
+
+        /* Added access rights */
+        foreach (array_diff($xacl, $both) as $uid) {
+            if (!empty($uid)) {
+                $result = $this->_add($filename, $uid);
+                if (is_a($result, 'PEAR_Error')) {
+                    return $result;
+                }
+            }
+        }
+
+        return true;
+    }
+}
+
+/**
+ * A representation of a cache file.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php,v 1.27 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_Cache_File {
+
+    /**
+     * The suffix of this cache file.
+     *
+     * @var string
+     */
+    var $_suffix = '';
+
+    /**
+     * Name of the cache file.
+     *
+     * @var string
+     */
+    var $_filename;
+
+    /**
+     * Full path to the cache file.
+     *
+     * @var string
+     */
+    var $_file;
+
+    /**
+     * Cache file version.
+     *
+     * @var int
+     */
+    var $_version = 1;
+
+    /**
+     * Construct the Horde_Kolab_FreeBusy_Cache_File instance.
+     *
+     * @param string $cache_dir The path to the cache direcory.
+     * @param string $filename  The file name of the cache file.
+     * @param string $suffix    The suffix of the cache file name.
+     */
+    function Horde_Kolab_FreeBusy_Cache_File($cache_dir, $filename, $suffix = null)
+    {
+        if (!empty($suffix)) {
+            $this->_suffix = $suffix;
+        }
+
+        $this->_cache_dir = $cache_dir;
+        $this->_filename  = $filename;
+        $this->_file = $this->_cache_dir . '/' . $this->_filename . '.' . $this->_suffix;
+    }
+
+    /**
+     * Get the full path to the cache file.
+     *
+     * @return string The full path to the file.
+     */
+    function getFile()
+    {
+        return $this->_file;
+    }
+
+    /**
+     * Clean the cache file contents.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function purge()
+    {
+        if (file_exists($this->_file)) {
+            $result = @unlink($this->_file);
+            if (!$result) {
+                return PEAR::raiseError(sprintf("Failed removing file %s",
+                                                $this->_file));
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Store data in the cache file.
+     *
+     * @param mixed $data A reference to the data object.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function store(&$data)
+    {
+        /* Create directories if missing */
+        $fbdirname = dirname($this->_file);
+        if (!is_dir($fbdirname)) {
+            $result = $this->_makeTree($fbdirname);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+
+        /* Store the cache data */
+        $fh = fopen($this->_file, 'w');
+        if (!$fh) {
+            return PEAR::raiseError(sprintf("Failed creating cache file %s!",
+                                            $this->_file));
+        }
+        fwrite($fh, serialize(array('version' => $this->_version,
+                                    'data' => $data)));
+        fclose($fh);
+        return true;
+    }
+
+    /**
+     * Load data from the cache file.
+     *
+     * @return mixed|PEAR_Error The data retrieved from the cache file.
+     */
+    function &load()
+    {
+        $file = @file_get_contents($this->_file);
+        if ($file === false) {
+            return PEAR::raiseError(sprintf("%s failed reading cache file %s!",
+                                            get_class($this), $this->_file));
+        }
+        $cache = @unserialize($file);
+        if ($cache === false) {
+            return PEAR::raiseError(sprintf("%s failed to unserialize cache data from file %s!",
+                                            get_class($this), $this->_file));
+        }
+        if (!isset($cache['version'])) {
+            return PEAR::raiseError(sprintf("Cache file %s lacks version data!",
+                                            $this->_file));
+        }
+        $this->_version = $cache['version'];
+        if (!isset($cache['data'])) {
+            return PEAR::raiseError(sprintf("Cache file %s lacks data!",
+                                            $this->_file));
+        }
+        if ($cache['version'] != $this->_version) {
+            return PEAR::raiseError(sprintf("Cache file %s has version %s while %s is required!",
+                                            $this->_file, $cache['version'], $this->_version));
+        }
+        return $cache['data'];
+    }
+
+    /**
+     * Generate a tree of directories.
+     *
+     * @param string $dirname The path to a directory that should exist.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function _maketree($dirname)
+    {
+        $base = substr($dirname, 0, strrpos($dirname, '/'));
+        $base = str_replace(".", "^", $base);
+        if (!empty($base) && !is_dir($base)) {
+            $result = $this->_maketree($base);
+            if (is_a($result, 'PEAR_Error')) {
+                return $result;
+            }
+        }
+        if (!file_exists($dirname)) {
+            $result = @mkdir($dirname, 0755);
+            if (!$result) {
+                return PEAR::raiseError(sprintf("Error creating directory %s", $dirname));
+            }
+        }
+        return true;
+    }
+}
+
+/**
+ * A cache file for partial free/busy information.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php,v 1.27 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_Cache_File_pvcal extends Horde_Kolab_FreeBusy_Cache_File {
+
+    /**
+     * The suffix of this cache file.
+     *
+     * @var string
+     */
+    var $_suffix = 'pvc';
+
+    /**
+     * Store partial free/busy infomation in the cache file.
+     *
+     * @param Horde_iCalendar $pvcal A reference to the data object.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function storePVcal(&$pvcal)
+    {
+        return $this->store($pvcal);
+    }
+
+    /**
+     * Load partial free/busy data from the cache file.
+     *
+     * @param boolean $extended Should the extended information be retrieved?
+     *
+     * @return Horde_iCalendar|PEAR_Error The data retrieved from the cache file.
+     */
+    function &loadPVcal($extended)
+    {
+        $pvcal = $this->load();
+        if (is_a($pvcal, 'PEAR_Error')) {
+            return $pvcal;
+        }
+        if (!$extended) {
+            $components = &$pvcal->getComponents();
+            foreach ($components as $component) {
+                if ($component->getType() == 'vFreebusy') {
+                    $component->_extraParams = array();
+                }
+            }
+        }
+        return $pvcal;
+    }
+
+    /**
+     * Return the last modification date of the cache file.
+     *
+     * @return int The last modification date.
+     */
+    function getMtime()
+    {
+        return filemtime($this->_file);
+    }
+}
+
+/**
+ * A cache file for complete free/busy information.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php,v 1.27 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_Cache_File_vcal extends Horde_Kolab_FreeBusy_Cache_File {
+
+    /**
+     * The suffix of this cache file.
+     *
+     * @var string
+     */
+    var $_suffix = 'vc';
+
+    /**
+     * Cache file version.
+     *
+     * @var int
+     */
+    var $_version = 2;
+
+    /**
+     * Cached data.
+     *
+     * @var array
+     */
+    var $_data;
+
+    /**
+     * Construct the Horde_Kolab_FreeBusy_Cache_File_vcal instance.
+     *
+     * @param string  $cache_dir The path to the cache direcory.
+     * @param string  $filename  The file name of the cache file.
+     * @param boolean $extended  Does the cache hold extended data?
+     */
+    function Horde_Kolab_FreeBusy_Cache_File_vcal($cache_dir, $filename, $extended)
+    {
+        $extension = empty($extended) ? 'vc' : 'xvc';
+        parent::Horde_Kolab_FreeBusy_Cache_File($cache_dir, $filename, $extension);
+    }
+
+    /**
+     * Store free/busy infomation in the cache file.
+     *
+     * @param Horde_iCalendar $vcal   A reference to the data object.
+     * @param array           $mtimes A list of modification times for the
+     *                                partial free/busy cache times.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function storeVcal(&$vcal, &$mtimes)
+    {
+        $data = array('vcal' => $vcal,
+                      'mtimes' => $mtimes);
+        return $this->store($data);
+    }
+
+    /**
+     * Load the free/busy information from the cache.
+     *
+     * @return Horde_iCalendar|PEAR_Error The retrieved free/busy information.
+     */
+    function &loadVcal()
+    {
+        if ($this->_data) {
+            return $this->_data;
+        }
+
+        $result = $this->load();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $this->_data = $result['vcal'];
+
+        return $this->_data;
+    }
+
+    /**
+     * Check if the cached free/busy expired.
+     *
+     * @param array $files A list of partial free/busy cache files.
+     *
+     * @return boolean|PEAR_Error True if the cache expired.
+     */
+    function expired($files)
+    {
+        $result = $this->load();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        /* Check the cache version */
+        if ($this->_version < 2) {
+            return true;
+        }
+
+        $this->_data = $result['vcal'];
+
+        /* Files changed? */
+        $keys = array_keys($result['mtimes']);
+        $changes = array_diff($keys, $files);
+        if (count($keys) != count($files) || !empty($changes)) {
+            return true;
+        }
+
+        /* Check the file mtimes */
+        foreach ($files as $file) {
+            if (filemtime($result['mtimes'][$file][0]) != $result['mtimes'][$file][1]) {
+                return true;
+            }
+        }
+
+        /* Older than three days? */
+        $components = $this->_data->getComponents();
+        foreach ($components as $component) {
+            if ($component->getType() == 'vFreebusy') {
+                $attr = $component->getAttribute('DTSTAMP');
+                if (!empty($attr) && !is_a($attr, 'PEAR_Error')) {
+                    //Should be configurable
+                    if (time() - (int)$attr > 259200) {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+}
+
+/**
+ * A cache file for ACLs. This serves as a buffer between the DB based
+ * ACL storage and is required to hold the old ACL list for updates to
+ * the DB based cache.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php,v 1.27 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_Cache_File_acl extends Horde_Kolab_FreeBusy_Cache_File {
+
+    /**
+     * The suffix of this cache file.
+     *
+     * @var string
+     */
+    var $_suffix = 'acl';
+
+    /**
+     * Link to the ACL stored in a data base.
+     *
+     * @var Horde_Kolab_FreeBusy_Cache_DB
+     */
+    var $_acls;
+
+    /**
+     * Construct the Horde_Kolab_FreeBusy_Cache_File_acl instance.
+     *
+     * @param string $cache_dir The path to the cache direcory.
+     * @param string $filename  The file name of the cache file.
+     */
+    function Horde_Kolab_FreeBusy_Cache_File_acl($cache_dir, $filename)
+    {
+        $this->_acls = &Horde_Kolab_FreeBusy_Cache_DB::singleton('acl', $cache_dir);
+        parent::Horde_Kolab_FreeBusy_Cache_File($cache_dir, $filename, 'acl');
+    }
+
+    /**
+     * Clean the cache file contents.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function purge()
+    {
+        $oldacl = $this->load();
+        if (is_a($oldacl, 'PEAR_Error')) {
+            $oldacl = array();
+        }
+
+        $result = $this->_acls->store($this->_filename, array(), $oldacl, false);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        return parent::purge();
+    }
+
+    /**
+     * Store a new ACL.
+     *
+     * @param array  $acl       The new ACL.
+     * @param string $relevance Folder relevance.
+     * @param string $append    Should old entries be purged?
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function storeACL(&$acl, $relevance, $append = false)
+    {
+        if (!$append) {
+            $oldacl = $this->load();
+            if (is_a($oldacl, 'PEAR_Error')) {
+                $oldacl = array();
+            }
+            $acl = array_merge($oldacl, $acl);
+        } else {
+            $oldacl = array();
+        }
+
+        /* Handle relevance */
+        switch ($relevance) {
+        case 'readers':
+            $perm = 'r';
+            break;
+        case 'nobody':
+            $perm = false;
+            break;
+        case 'admins':
+        default:
+            $perm = 'a';
+        }
+
+        $result = $this->_acls->store($this->_filename, $acl, $oldacl, $perm);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $result = $this->store($acl);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        return true;
+    }
+}
+
+/**
+ * A cache file for extended ACLs. This serves as a buffer between the
+ * DB based ACL storage and is required to hold the old ACL list for
+ * updates to the DB based cache.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Cache.php,v 1.27 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_Cache_File_xacl extends Horde_Kolab_FreeBusy_Cache_File {
+
+    /**
+     * The suffix of this cache file.
+     *
+     * @var string
+     */
+    var $_suffix = 'xacl';
+
+    /**
+     * Link to the ACL stored in a data base.
+     *
+     * @var Horde_Kolab_FreeBusy_Cache_DB
+     */
+    var $_xacls;
+
+    /**
+     * Construct the Horde_Kolab_FreeBusy_Cache_File_xacl instance.
+     *
+     * @param string $cache_dir The path to the cache direcory.
+     * @param string $filename  The file name of the cache file.
+     */
+    function Horde_Kolab_FreeBusy_Cache_File_xacl($cache_dir, $filename)
+    {
+        $this->_xacls = &Horde_Kolab_FreeBusy_Cache_DB::singleton('xacl', $cache_dir);
+        parent::Horde_Kolab_FreeBusy_Cache_File($cache_dir, $filename, 'xacl');
+    }
+
+    /**
+     * Clean the cache file contents.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function purge()
+    {
+        $oldxacl = $this->load();
+        if (is_a($oldxacl, 'PEAR_Error')) {
+            $oldxacl = '';
+        }
+
+        $result = $this->_xacls->store($this->_filename, '', $oldxacl);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        return parent::purge();
+    }
+
+    /**
+     * Store a new extended ACL.
+     *
+     * @param array $xacl  The new extended ACL.
+     * @param array $acl   General ACL for the folder.
+     *
+     * @return boolean|PEAR_Error True if successful.
+     */
+    function storeXACL(&$xacl, &$acl)
+    {
+        $oldxacl = $this->load();
+        if (is_a($oldxacl, 'PEAR_Error')) {
+            $oldxacl = '';
+        }
+
+        /* Users with read access to the folder may also access the extended information */
+        foreach ($acl as $user => $ac) {
+            if (strpos($ac, 'r') !== false) {
+                if (!empty($user)) {
+                    $xacl .= ' ' . $user;
+                }
+            }
+        }
+
+        $result = $this->_xacls->store($this->_filename, $xacl, $oldxacl);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+
+        $result = $this->store($xacl);
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        }
+        return true;
+    }
+}
diff --git a/framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Imap.php b/framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Imap.php
new file mode 100644 (file)
index 0000000..c5c8c48
--- /dev/null
@@ -0,0 +1,544 @@
+<?php
+/**
+ * IMAP access for Kolab free/busy.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Imap.php,v 1.10 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * @package Kolab_FreeBusy
+ */
+
+/** We need the Kolab Stores library for connecting to the IMAP storage. */
+require_once 'Horde/Kolab/Storage/List.php';
+
+/** We need the Date library for event handling. */
+require_once 'Horde/Date.php';
+
+/** We need the Recurrence library for recurrence handling. */
+require_once 'Horde/Date/Recurrence.php';
+
+/** Event status - Taken from Kronolith*/
+define('KRONOLITH_STATUS_NONE', 0);
+define('KRONOLITH_STATUS_TENTATIVE', 1);
+define('KRONOLITH_STATUS_CONFIRMED', 2);
+define('KRONOLITH_STATUS_CANCELLED', 3);
+define('KRONOLITH_STATUS_FREE', 4);
+
+/**
+ * The Horde_Kolab_Freebusy class provides a library for quickly
+ * generating free/busy information from the Kolab IMAP data.
+ *
+ * This class is a merged result from the Kolab free/busy package and
+ * the Horde::Kronolith free/busy driver.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Imap.php,v 1.10 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @author  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_Imap {
+
+    /**
+     * Our list of Kolab server IMAP folders.
+     *
+     * @var Kolab_List
+     */
+    var $_kolab = null;
+
+    /**
+     * The folder we are generating free/busy information for.
+     *
+     * @var Kolab_Folder
+     */
+    var $_folder;
+
+    /**
+     * The link to the folder data.
+     *
+     * @var Kolab_Data
+     */
+    var $_data;
+
+    /**
+     * Is this store relevant only for users or admins?
+     *
+     * @var string
+     */
+    var $_relevance;
+
+    /**
+     * Store ACLs.
+     *
+     * @var string
+     */
+    var $_acl;
+
+    /**
+     * Store extended attributes ACL.
+     *
+     * @var string
+     */
+    var $_xacl;
+
+    /**
+     * Initialize the free/busy IMAP handler.
+     */
+    function Horde_Kolab_FreeBusy_Imap()
+    {
+        $this->_kolab = &Kolab_List::singleton();
+    }
+
+    /**
+     * Connect to IMAP.
+     *
+     * This function has been derived from the synchronize() function
+     * in the Kolab driver for Kronolith.
+     *
+     * @param string $folder         The folder to generate free/busy data for.
+     */
+    function connect($folder)
+    {
+        // Connect to the Kolab backend
+        $this->_folder = $this->_kolab->getFolder($folder);
+        if (is_a($this->_folder, 'PEAR_Error')) {
+            return $this->_folder;
+        }
+
+        $this->_data = $this->_folder->getData();
+        if (is_a($this->_data, 'PEAR_Error')) {
+            return $this->_data;
+        }
+        if (!$this->_folder->exists()) {
+            return PEAR::raiseError(sprintf(_("Folder %s does not exist!"), $folder));
+        }
+        $type = $this->_folder->getType();
+        if (is_a($type, 'PEAR_Error')) {
+            return $type;
+        }
+        if ($type != 'event') {
+            return PEAR::raiseError(sprintf(_("Folder %s has type \"%s\" not \"event\"!"),
+                                            $folder, $type));
+        }
+    }
+
+    /**
+     * Lists all events in the time range, optionally restricting
+     * results to only events with alarms.
+     *
+     * Taken from the Kolab driver for Kronolith.
+     *
+     * @param Horde_Date $startInterval  Start of range date object.
+     * @param Horde_Date $endInterval    End of range data object.
+     *
+     * @return array  Events in the given time range.
+     */
+    function listEvents($startDate = null, $endDate = null)
+    {
+        $objects = $this->_data->getObjects();
+        if (is_a($objects, 'PEAR_Error')) {
+            return $objects;
+        }
+
+        if (is_null($startDate)) {
+            $startDate = new Horde_Date(array('mday' => 1, 'month' => 1, 'year' => 0000));
+        }
+        if (is_null($endDate)) {
+            $endDate = new Horde_Date(array('mday' => 31, 'month' => 12, 'year' => 9999));
+        }
+        $startts = $startDate->timestamp();
+        $endts = $endDate->timestamp();
+
+        $result = array();
+
+        foreach($objects as $object) {
+            /* check if event period intersects with given period */
+            if (!(($object['start-date'] > $endts) ||
+                  ($object['end-date'] < $startts))) {
+                $event = new Kolab_Event($object);
+                $result[] = $event;
+                continue;
+            }
+
+            /* do recurrence expansion if not keeping anyway */
+            if (isset($object['recurrence'])) {
+                $event = new Kolab_Event($object);
+                $next = $event->recurrence->nextRecurrence($startDate);
+                while ($next !== false &&
+                       $event->recurrence->hasException($next->year, $next->month, $next->mday)) {
+                    $next->mday++;
+                    $next = $event->recurrence->nextRecurrence($next);
+                }
+
+                if ($next !== false) {
+                    $duration = $next->timestamp() - $event->start->timestamp();
+                    $next_end = new Horde_Date($event->end->timestamp() + $duration);
+
+                    if ((!(($endDate->compareDateTime($next) < 0) ||
+                           ($startDate->compareDateTime($next_end) > 0)))) {
+                        $result[] = $event;
+                    }
+
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Fetch the relevance of this calendar folder.
+     *
+     * @return string|PEAR_Error Relevance of this folder.
+     */
+    function getRelevance() {
+
+        /* cached? */
+        if (isset($this->_relevance)) {
+            return $this->_relevance;
+        }
+
+        $annotation = $this->_folder->getKolabAttribute('incidences-for');
+        if (is_a($annotation, 'PEAR_Error')) {
+            return $annotation;
+        }
+
+        if (empty($annotation)) {
+            Horde::logMessage(sprintf('No relevance value found for %s', $this->_folder->name),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $this->_relevance = 'admins';
+        } else {
+            Horde::logMessage(sprintf('Relevance for %s is %s', $this->_folder->name, $annotation),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $this->_relevance = $annotation;
+        }
+        return $this->_relevance;
+    }
+
+    /**
+     * Fetch the ACL of this calendar folder.
+     *
+     * @return array|PEAR_Error IMAP ACL of this folder.
+     */
+    function getACL() {
+
+        /* cached? */
+        if (isset($this->_acl)) {
+            return $this->_acl;
+        }
+
+        $perm = $this->_folder->getPermission();
+        if (is_a($perm, 'PEAR_Error')) {
+            return $perm;
+        }
+
+        $acl = &$perm->acl;
+        if (empty($acl)) {
+            Horde::logMessage(sprintf('No ACL found for %s', $this->_folder->name),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $this->_acl = array();
+        } else {
+            Horde::logMessage(sprintf('ACL for %s is %s',
+                                      $this->_folder->name, serialize($acl)),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $this->_acl = $acl;
+        }
+        return $this->_acl;
+    }
+
+    /**
+     * Fetch the extended ACL of this calendar folder.
+     *
+     * @return array|PEAR_Error Extended ACL of this folder.
+     */
+    function getExtendedACL() {
+
+        /* cached? */
+        if (isset($this->_xacl)) {
+            return $this->_xacl;
+        }
+
+        $annotation = $this->_folder->getXfbaccess();
+        if (is_a($annotation, 'PEAR_Error')) {
+            return $annotation;
+        }
+
+        if (empty($annotation)) {
+            Horde::logMessage(sprintf('No extended ACL value found for %s', $this->_folder->name),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $this->_xacl = '';
+        } else {
+            $annotation = join(' ', $annotation);
+            Horde::logMessage(sprintf('Extended ACL for %s is %s', $this->_folder->name, $annotation),
+                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            $this->_xacl = $annotation;
+        }
+        return $this->_xacl;
+    }
+
+    /**
+     * Generates the free/busy text for $calendar. Cache it for at least an
+     * hour, as well.
+     *
+     * @param integer $startstamp     The start of the time period to retrieve.
+     * @param integer $endstamp       The end of the time period to retrieve.
+     * @param integer $fbpast         The number of days that free/busy should
+     *                                be calculated for the past
+     * @param integer $fbfuture       The number of days that free/busy should
+     *                                be calculated for the future
+     * @param string  $user           Set organizer to this user.
+     * @param string  $cn             Set the common name of this user.
+     *
+     * @return Horde_iCalendar  The iCal object or a PEAR error.
+     */
+    function &generate($startstamp = null, $endstamp = null,
+                       $fbpast = 0, $fbfuture = 60,
+                       $user = null, $cn = null)
+    {
+        /* Get the iCalendar library at this point */
+        require_once 'Horde/iCalendar.php';
+
+        /* Default the start date to today. */
+        if (is_null($startstamp)) {
+            $month = date('n');
+            $year = date('Y');
+            $day = date('j');
+
+            $startstamp = mktime(0, 0, 0, $month, $day - $fbpast, $year);
+        }
+
+        /* Default the end date to the start date + freebusy_days. */
+        if (is_null($endstamp) || $endstamp < $startstamp) {
+            $month = date('n');
+            $year = date('Y');
+            $day = date('j');
+
+            $endstamp = mktime(0, 0, 0,
+                               $month,
+                               $day + $fbfuture,
+                               $year);
+        }
+
+        Horde::logMessage(sprintf('Creating free/busy information from %s to %s',
+                                  $startstamp, $endstamp), __FILE__, __LINE__,
+                          PEAR_LOG_DEBUG);
+
+        /* Fetch events. */
+        $startDate = new Horde_Date($startstamp);
+        $endDate = new Horde_Date($endstamp);
+        $events = $this->listEvents($startDate, $endDate);
+        if (is_a($events, 'PEAR_Error')) {
+            return $events;
+        }
+
+        /* Create the new iCalendar. */
+        $vCal = new Horde_iCalendar();
+        $vCal->setAttribute('PRODID', '-//kolab.org//NONSGML Kolab Server 2//EN');
+        $vCal->setAttribute('METHOD', 'PUBLISH');
+
+        /* Create new vFreebusy. */
+        $vFb = &Horde_iCalendar::newComponent('vfreebusy', $vCal);
+        $params = array();
+        if ($cn) {
+            $params['cn'] = $cn;
+        }
+        $vFb->setAttribute('ORGANIZER', 'MAILTO:' . $user, $params);
+
+        $vFb->setAttribute('DTSTAMP', $_SERVER['REQUEST_TIME']);
+        $vFb->setAttribute('DTSTART', $startstamp);
+        $vFb->setAttribute('DTEND', $endstamp);
+        // URL is not required, so out it goes...
+        //$vFb->setAttribute('URL', Horde::applicationUrl('fb.php?u=' . $share->get('owner'), true, -1));
+
+        /* Add all the busy periods. */
+        foreach ($events as $event) {
+            if ($event->hasStatus(KRONOLITH_STATUS_FREE)) {
+                continue;
+            }
+
+            $duration = $event->end->timestamp() - $event->start->timestamp();
+            $extra = array('X-UID'      => base64_encode($event->eventID),
+                           'X-SUMMARY'  => base64_encode($event->private ? '' : $event->title),
+                           'X-LOCATION' => base64_encode($event->private ? '' : $event->location));
+
+            /* Make sure that we're using the current date for recurring
+             * events. */
+            if ($event->recurs()) {
+                $startThisDay = mktime($event->start->hour,
+                                       $event->start->min,
+                                       $event->start->sec,
+                                       date('n', $day),
+                                       date('j', $day),
+                                       date('Y', $day));
+            } else {
+                $startThisDay = $event->start->timestamp($extra);
+            }
+            if (!$event->recurs()) {
+                $vFb->addBusyPeriod('BUSY', $startThisDay, null, $duration, $extra);
+            } else {
+                $next = $event->recurrence->nextRecurrence($startDate);
+                while ($next) {
+                    if ($endDate->compareDateTime($next) < 0) {
+                        break;
+                    }
+                    if (!$event->recurrence->hasException($next->year, $next->month, $next->mday)) {
+                        $vFb->addBusyPeriod('BUSY', $next->timestamp(), null, $duration, $extra);
+                    }
+                    $next->mday++;
+                    $next = $event->recurrence->nextRecurrence($next);
+                }
+            }
+        }
+
+        /* Remove the overlaps. */
+        $vFb->simplify();
+        $vCal->addComponent($vFb);
+
+        return $vCal;
+    }
+}
+
+/**
+ * A reduced event representation derived from the Kronolith event
+ * representation.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Imap.php,v 1.10 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @package Kolab_FreeBusy
+ */
+class Kolab_Event {
+
+    /**
+     * The driver unique identifier for this event.
+     *
+     * @var string
+     */
+    var $eventID = null;
+
+    /**
+     * The start time of the event.
+     *
+     * @var Horde_Date
+     */
+    var $start;
+
+    /**
+     * The end time of the event.
+     *
+     * @var Horde_Date
+     */
+    var $end;
+
+    /**
+     * The title of this event.
+     *
+     * @var string
+     */
+    var $title = '';
+
+    /**
+     * The location this event occurs at.
+     *
+     * @var string
+     */
+    var $location = '';
+
+    /**
+     * Whether the event is private.
+     *
+     * @var boolean
+     */
+    var $private = false;
+
+    function Kolab_Event($event)
+    {
+        $this->eventID = $event['uid'];
+
+        $this->start = new Horde_Date($event['start-date']);
+        $this->end = new Horde_Date($event['end-date']);
+
+        if (isset($event['summary'])) {
+            $this->title = $event['summary'];
+        }
+
+        if (isset($event['location'])) {
+            $this->location = $event['location'];
+        }
+
+        if ($event['sensitivity'] == 'private' || $event['sensitivity'] == 'confidential') {
+            $this->private = true;
+        }
+
+        if (isset($event['show-time-as'])) {
+            switch ($event['show-time-as']) {
+                case 'free':
+                    $this->status = KRONOLITH_STATUS_FREE;
+                    break;
+
+                case 'tentative':
+                    $this->status = KRONOLITH_STATUS_TENTATIVE;
+                    break;
+
+                case 'busy':
+                case 'outofoffice':
+                default:
+                    $this->status = KRONOLITH_STATUS_CONFIRMED;
+            }
+        } else {
+            $this->status = KRONOLITH_STATUS_CONFIRMED;
+        }
+
+        // Recurrence
+        if (isset($event['recurrence'])) {
+            $this->recurrence = new Horde_Date_Recurrence($this->start);
+            $this->recurrence->fromHash($event['recurrence']);
+        }
+
+    }
+
+    /**
+     * Returns whether this event is a recurring event.
+     *
+     * @return boolean  True if this is a recurring event.
+     */
+    function recurs()
+    {
+        return isset($this->recurrence) &&
+            !$this->recurrence->hasRecurType(Horde_Date_Recurrence::RECUR_NONE);
+    }
+
+    /**
+     * Sets the global UID for this event.
+     *
+     * @param string $uid  The global UID for this event.
+     */
+    function setUID($uid)
+    {
+        $this->_uid = $uid;
+    }
+
+    /**
+     * Checks whether the events status is the same as the specified value.
+     *
+     * @param integer $status  The status value to check against.
+     *
+     * @return boolean  True if the events status is the same as $status.
+     */
+    function hasStatus($status)
+    {
+        return ($status == $this->status);
+    }
+
+}
diff --git a/framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Report.php b/framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Report.php
new file mode 100644 (file)
index 0000000..7633bd8
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+/**
+ * A view for regenerating the Kolab Free/Busy cache.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/Report.php,v 1.2 2009/04/02 20:12:16 wrobel Exp $
+ *
+ * Copyright 2009 Klarälvdalens Datakonsult AB
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @package Kolab_FreeBusy
+ */
+
+class Horde_Kolab_FreeBusy_Report {
+
+    var $_break = '<br/>';
+
+    var $_errors = array();
+
+    function Horde_Kolab_FreeBusy_Report()
+    {
+        if (PHP_SAPI == 'cli') {
+            $this->_break = "\n";
+
+            /* Display errors if we are working on the command line */
+            ini_set('display_errors', 1);
+
+            /** Don't report notices */ 
+            error_reporting(E_ALL & ~E_NOTICE);
+        }
+    }
+
+    function start()
+    {
+        echo _("Starting to regenerate the free/busy cache...");
+        $this->linebreak(2);
+    }
+
+    function success($calendar)
+    {
+        echo sprintf(_("Successfully regenerated calendar \"%s\"!"),
+                     $calendar);
+        $this->linebreak(1);
+    }
+
+    function failure($calendar, $error)
+    {
+        $this->_errors[] = sprintf(_("Failed regenerating calendar %s: %s"),
+                                   $calendar, $error->getMessage());
+    }
+
+    function stop()
+    {
+        if (!empty($this->_errors)) {
+            $this->linebreak(1);
+            echo _("Errors:");
+            $this->linebreak(1);
+            foreach ($this->_errors as $error) {
+                echo $error;
+            }
+            return false;
+        } else {
+            $this->linebreak(1);
+            echo _("Successfully regenerated all calendar caches!");
+            $this->linebreak(1);
+            return true;
+        }
+    }
+
+    function linebreak($count = 1)
+    {
+        for ($i = 0; $i < $count; $i++) {
+            echo $this->_break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/View.php b/framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/View.php
new file mode 100644 (file)
index 0000000..cdaf71c
--- /dev/null
@@ -0,0 +1,291 @@
+<?php
+/**
+ * Provides simple views.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/View.php,v 1.6 2008/12/12 15:00:07 wrobel Exp $
+ *
+ * @package Kolab_FreeBusy
+ */
+
+/**
+ * The Horde_Kolab_FreeBusy_View:: class renders data.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/View.php,v 1.6 2008/12/12 15:00:07 wrobel Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_View {
+
+    /**
+     * The data that should get rendered.
+     *
+     * @var array
+     */
+    var $_data;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data The data to display
+     */
+    function Horde_Kolab_FreeBusy_View(&$data)
+    {
+        $this->_data = $data;
+    }
+
+    /**
+     * Render the data.
+     */
+    function render()
+    {
+        echo 'Not implemented!';
+    }
+}
+
+/**
+ * The Horde_Kolab_FreeBusy_View_vfb:: class renders free/busy data.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/View.php,v 1.6 2008/12/12 15:00:07 wrobel Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_View_vfb extends Horde_Kolab_FreeBusy_View {
+
+    /**
+     * The free/busy data that should be displayed.
+     *
+     * @var Horde_iCalendar
+     */
+    var $_vfb;
+
+    /**
+     * Current timestamp.
+     *
+     * @var int
+     */
+    var $_ts;
+
+    /**
+     * Constructor.
+     *
+     * @param Horde_iCalendar $vfb The free/busy data to display.
+     */
+    function Horde_Kolab_FreeBusy_View_vfb(&$data)
+    {
+
+        $data['vfb'] = $data['fb']->exportvCalendar();
+
+        $ts = time();
+
+        $components = &$data['fb']->getComponents();
+        foreach ($components as $component) {
+            if ($component->getType() == 'vFreebusy') {
+                $attr = $component->getAttribute('DTSTAMP');
+                if (!empty($attr) && !is_a($attr, 'PEAR_Error')) {
+                    $ts = $attr;
+                    break;
+                }
+            }
+        }
+
+        $data['ts'] = $ts;
+
+        Horde_Kolab_FreeBusy_View::Horde_Kolab_FreeBusy_View($data);
+    }
+
+    /**
+     * Display the free/busy information.
+     *
+     * @param string $content File name of the offered file.
+     */
+    function render()
+    {
+        global $conf;
+
+        if (!empty($conf['fb']['send_content_type'])) {
+            $send_content_type = $conf['fb']['send_content_type'];
+        } else {
+            $send_content_type = false;
+        }
+
+        if (!empty($conf['fb']['send_content_length'])) {
+            $send_content_length = $conf['fb']['send_content_length'];
+        } else {
+            $send_content_length = false;
+        }
+
+        if (!empty($conf['fb']['send_content_disposition'])) {
+            $send_content_disposition = $conf['fb']['send_content_disposition'];
+        } else {
+            $send_content_disposition = false;
+        }
+
+        /* Ensure that the data doesn't get cached along the way */
+        header('Cache-Control: no-store, no-cache, must-revalidate');
+        header('Cache-Control: post-check=0, pre-check=0', false);
+        header('Pragma: no-cache');
+        header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+        header('Last-Modified: ' . gmdate("D, d M Y H:i:s", $this->_data['ts']) . ' GMT');
+        header('Pragma: public');
+        header('Content-Transfer-Encoding: none');
+        if ($send_content_type) {
+            header('Content-Type: text/calendar');
+        }
+        if ($send_content_length) {
+            header('Content-Length: ' . strlen($this->_data['vfb']));
+        }
+        if ($send_content_disposition) {
+            header('Content-Disposition: attachment; filename="' . $this->_data['name'] . '"');
+        }
+
+        echo $this->_data['vfb'];
+
+        exit(0);
+    }
+}
+
+/** Error types */
+define('FREEBUSY_ERROR_NOTFOUND', 0);
+define('FREEBUSY_ERROR_UNAUTHORIZED', 1);
+define('FREEBUSY_ERROR_SERVER', 2);
+
+/**
+ * The Horde_Kolab_FreeBusy_View_error:: class provides error pages for the 
+ * Kolab free/busy system.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/FreeBusy/View.php,v 1.6 2008/12/12 15:00:07 wrobel Exp $
+ *
+ * Copyright 2004-2008 Klarälvdalens Datakonsult AB
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ *
+ * @author  Gunnar Wrobel <wrobel@pardus.de>
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_View_error extends Horde_Kolab_FreeBusy_View {
+
+    /**
+     * Display the error information.
+     */
+    function render()
+    {
+        switch ($this->_data['type']) {
+        case FREEBUSY_ERROR_NOTFOUND:
+            $this->notFound($this->_data['error']);
+            exit(1);
+        case FREEBUSY_ERROR_UNAUTHORIZED:
+            $this->unauthorized($this->_data['error']);
+            exit(1);
+        case FREEBUSY_ERROR_SERVER:
+            $this->serverError($this->_data['error']);
+            exit(1);
+        }
+    }
+
+    /**
+     * Deliver a "Not Found" page
+     *
+     * @param PEAR_Error $error    The error.
+     */
+    function notFound($error)
+    {
+        $headers = array('HTTP/1.0 404 Not Found');
+        if (isset($_SERVER['REQUEST_URI'])) {
+            $url = htmlentities($_SERVER['REQUEST_URI']);
+        } else {
+            $url = '/';
+        }
+        $message = sprintf(_("The requested URL %s was not found on this server."), $url);
+
+        $this->_errorPage($error, $headers, _("404 Not Found"), _("Not found"), $message);
+    }
+
+    /**
+     * Deliver a "Unauthorized" page
+     *
+     * @param PEAR_Error $error    The error.
+     */
+    function unauthorized($error)
+    {
+        global $conf;
+
+        if (!empty($conf['kolab']['imap']['maildomain'])) {
+            $email_domain = $conf['kolab']['imap']['maildomain'];
+        } else {
+            $email_domain = 'localhost';
+        }
+
+        $headers = array('WWW-Authenticate: Basic realm="freebusy-' . $email_domain . '"',
+                         'HTTP/1.0 401 Unauthorized');
+
+        $this->_errorPage($error, $headers, _("401 Unauthorized"), _("Unauthorized"),
+                  _("You are not authorized to access the requested URL."));
+    }
+
+    /**
+     * Deliver a "Server Error" page
+     *
+     * @param PEAR_Error $error    The error.
+     */
+    function serverError($error)
+    {
+        $headers = array('HTTP/1.0 500 Server Error');
+        if (isset($_SERVER['REQUEST_URI'])) {
+            $url = htmlentities($_SERVER['REQUEST_URI']);
+        } else {
+            $url = '/';
+        }
+        $this->_errorPage($error, $headers, _("500 Server Error"), _("Error"),
+                  htmlentities($$url));
+    }
+
+    /**
+     * Deliver an error page
+     *
+     * @param PEAR_Error $error    The error.
+     * @param array      $headers  The HTTP headers to deliver with the response
+     * @param string     $title    The page title
+     * @param string     $headline The headline of the page
+     * @param string     $body     The message to display on the page
+     */
+    function _errorPage($error, $headers, $title, $headline, $body)
+    {
+        global $conf;
+
+        /* Print the headers */
+        foreach ($headers as $line) {
+            header($line);
+        }
+
+        /* Print the page */
+        echo "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n";
+        echo "<html><head><title>" . $title . "</title></head>\n";
+        echo "<body>\n";
+        echo "<h1>" . $headline . "</h1>\n";
+        echo "<p>" . $body . "</p>\n";
+        if (!empty($error)) {
+            echo "<hr><pre>" . $error->getMessage() . "</pre>\n";
+            Horde::logMessage($error, __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+        echo "<hr>\n";
+        echo isset($_SERVER['SERVER_SIGNATURE'])?$_SERVER['SERVER_SIGNATURE']:'' . "\n";
+        echo "</body></html>\n";
+    }
+}
+
diff --git a/framework/Kolab_FreeBusy/lib/Horde/Kolab/Test/FreeBusy.php b/framework/Kolab_FreeBusy/lib/Horde/Kolab/Test/FreeBusy.php
new file mode 100644 (file)
index 0000000..64c41e5
--- /dev/null
@@ -0,0 +1,249 @@
+<?php
+/**
+ * Base for PHPUnit scenarios.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/Test/FreeBusy.php,v 1.5 2009/04/25 19:39:38 wrobel Exp $
+ *
+ * PHP version 5
+ *
+ * @category Kolab
+ * @package  Kolab_Test
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Storage
+ */
+
+/**
+ *  We need the unit test framework
+ */
+require_once 'Horde/Kolab/Test/Storage.php';
+
+/**
+ * Base for PHPUnit scenarios.
+ *
+ * $Horde: framework/Kolab_FreeBusy/lib/Horde/Kolab/Test/FreeBusy.php,v 1.5 2009/04/25 19:39:38 wrobel Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Kolab
+ * @package  Kolab_Test
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Storage
+ */
+class Horde_Kolab_Test_FreeBusy extends Horde_Kolab_Test_Storage
+{
+
+    /**
+     * Prepare the configuration.
+     *
+     * @return NULL
+     */
+    public function prepareConfiguration()
+    {
+        $fh = fopen(HORDE_BASE . '/config/conf.php', 'w');
+        $data = <<<EOD
+\$conf['use_ssl'] = 2;
+\$conf['server']['name'] = \$_SERVER['SERVER_NAME'];
+\$conf['server']['port'] = \$_SERVER['SERVER_PORT'];
+\$conf['debug_level'] = E_ALL;
+\$conf['umask'] = 077;
+\$conf['compress_pages'] = true;
+\$conf['menu']['always'] = false;
+\$conf['portal']['fixed_blocks'] = array();
+\$conf['imsp']['enabled'] = false;
+
+/** Additional config variables required for a clean Horde setup */
+\$conf['session']['use_only_cookies'] = false;
+\$conf['session']['timeout'] = 0;
+\$conf['cookie']['path'] = '/';
+\$conf['cookie']['domain'] = \$_SERVER['SERVER_NAME'];
+\$conf['use_ssl'] = false;
+\$conf['session']['cache_limiter'] = 'nocache';
+\$conf['session']['name'] = 'Horde';
+\$conf['log']['enabled'] = false;
+\$conf['prefs']['driver'] = 'session';
+\$conf['auth']['driver'] = 'kolab';
+\$conf['share']['driver'] = 'kolab';
+\$conf['debug_level'] = E_ALL;
+
+/** Make the share driver happy */
+\$conf['kolab']['enabled'] = true;
+
+/** Ensure we still use the LDAP test driver */
+\$conf['kolab']['server']['driver'] = 'test';
+
+/** Ensure that we do not trigger on folder update */
+\$conf['kolab']['no_triggering'] = true;
+
+/** Storage location for the free/busy system */
+\$conf['fb']['cache_dir']             = '/tmp';
+\$conf['kolab']['freebusy']['server'] = 'https://fb.example.org/freebusy';
+
+/** Setup the virtual file system for Kolab */
+\$conf['vfs']['params']['all_folders'] = true;
+\$conf['vfs']['type'] = 'kolab';
+
+\$conf['kolab']['ldap']['phpdn'] = null;
+\$conf['fb']['use_acls'] = true;
+EOD;
+        fwrite($fh, "<?php\n" . $data);
+        fclose($fh);
+    }
+
+    /**
+     * Tear down testing
+     */
+    public function tearDown()
+    {
+        if (file_exists('/tmp/aclcache.db')) {
+            unlink('/tmp/aclcache.db');
+        }
+        if (file_exists('/tmp/xaclcache.db')) {
+            unlink('/tmp/xaclcache.db');
+        }
+        if (file_exists('/tmp/example^org')) {
+            $this->unlinkDir('/tmp/example^org');
+        }
+    }
+
+    function unlinkDir($dir)
+    {
+        if(!$dh = @opendir($dir)) {
+            return;
+        }
+        while (false !== ($obj = readdir($dh))) {
+            if($obj == '.' || $obj == '..') {
+                continue;
+            }
+            if (!@unlink($dir . '/' . $obj)) {
+                $this->unlinkDir($dir . '/' . $obj);
+            }
+        }
+        closedir($dh);
+        @rmdir($dir);
+
+        return;
+    } 
+
+    /**
+     * Handle a "given" step.
+     *
+     * @param array  &$world    Joined "world" of variables.
+     * @param string $action    The description of the step.
+     * @param array  $arguments Additional arguments to the step.
+     *
+     * @return mixed The outcome of the step.
+     */
+    public function runGiven(&$world, $action, $arguments)
+    {
+        switch($action) {
+        default:
+            return parent::runGiven($world, $action, $arguments);
+        }
+    }
+
+    /**
+     * Handle a "when" step.
+     *
+     * @param array  &$world    Joined "world" of variables.
+     * @param string $action    The description of the step.
+     * @param array  $arguments Additional arguments to the step.
+     *
+     * @return mixed The outcome of the step.
+     */
+    public function runWhen(&$world, $action, $arguments)
+    {
+        switch($action) {
+        case 'adding an event to a folder':
+            $world['result']['add_event'][] = $this->addEvent($arguments[0],
+                                                              $arguments[1]);
+            break;
+        case 'triggering the folder':
+            include_once 'Horde/Kolab/FreeBusy.php';
+
+            $_GET['folder']   = $arguments[0];
+            $_GET['extended'] = '1';
+
+            $fb = &new Horde_Kolab_FreeBusy();
+
+            $world['result']['trigger'] = $fb->trigger();
+
+            break;
+        case 'fetching the free/busy information for':
+            include_once 'Horde/Kolab/FreeBusy.php';
+
+            $_GET['uid']   = $arguments[0];
+            $_GET['extended'] = '1';
+
+            $fb = &new Horde_Kolab_FreeBusy();
+
+            $world['result']['fetch'] = $fb->fetch();
+
+            break;
+        default:
+            return parent::runWhen($world, $action, $arguments);
+        }
+    }
+
+    /**
+     * Handle a "then" step.
+     *
+     * @param array  &$world    Joined "world" of variables.
+     * @param string $action    The description of the step.
+     * @param array  $arguments Additional arguments to the step.
+     *
+     * @return mixed The outcome of the step.
+     */
+    public function runThen(&$world, $action, $arguments)
+    {
+        switch($action) {
+        case 'the fetch result should contain a free/busy time with summary':
+            $this->assertTrue($this->freeBusyContainsSummary($world['result']['fetch']->_data['fb']->findComponent('vfreebusy'),
+                                                             $arguments[0]));
+            break;
+        case 'the fetch result should not contain a free/busy time with summary':
+            $this->assertFalse($this->freeBusyContainsSummary($world['result']['fetch']->_data['fb']->findComponent('vfreebusy'),
+                                                             $arguments[0]));
+            break;
+        default:
+            return parent::runThen($world, $action, $arguments);
+        }
+    }
+
+    public function freeBusyContainsSummary($vfb, $summary)
+    {
+        $params = $vfb->getExtraParams();
+        $present = false;
+        foreach ($params as $event) {
+            if (isset($event['X-SUMMARY'])
+                && base64_decode($event['X-SUMMARY']) == $summary) {
+                $present = true;
+            }
+        }
+        return $present;
+    }
+
+    /**
+     * Add an event.
+     *
+     * @return NULL
+     */
+    public function addEvent($event, $folder)
+    {
+        include_once 'Horde/Kolab/Storage.php';
+
+        $folder = Kolab_Storage::getShare($folder, 'event');
+        $this->assertNoError($folder);
+        $data   = Kolab_Storage::getData($folder, 'event', 1);
+        $this->assertNoError($data);
+        /* Add the event */
+        $result = $data->save($event);
+        $this->assertNoError($result);
+        return $result;
+    }
+
+}
diff --git a/framework/Kolab_FreeBusy/package.xml b/framework/Kolab_FreeBusy/package.xml
new file mode 100644 (file)
index 0000000..3fc93e9
--- /dev/null
@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Kolab_FreeBusy</name>
+ <channel>pear.horde.org</channel>
+ <summary>A package for providing free/busy information.</summary>
+ <description>This package provides free/busy information for the
+ users of a Kolab server. A Kolab client changing calendar data in an
+ IMAP folder is required to call the triggering script provided within
+ this package via HTTP. This will refresh the cache maintained by this
+ package with partial free/busy data. This partial data sets are
+ finally combined to the complete free/busy information once a client
+ requests this data for a particular user.
+ </description>
+ <lead>
+  <name>Gunnar Wrobel</name>
+  <user>wrobel</user>
+  <email>p@rdus.de</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Thomas Jarosch</name>
+  <user>jarosch</user>
+  <email>thomas.jarosch@intra2net.com</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2009-04-25</date>
+ <version>
+  <release>0.1.5</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+ <notes>
+  * Adapted the package to the changes in Kolab_Server.
+  * kolab/issue3537 (Allowing xfb access to groups does not work)
+  * Improved handling of dba access.
+  * Extended support for more complex scenario testing.
+ </notes>
+ <contents>
+  <dir name="/">
+   <file name="COPYING" role="doc" />
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Kolab">
+      <file name="FreeBusy.php" role="php" />
+      <dir name="FreeBusy">
+       <file name="Access.php" role="php" />
+       <file name="Cache.php" role="php" />
+       <file name="Imap.php" role="php" />
+       <file name="Report.php" role="php" />
+       <file name="View.php" role="php" />
+      </dir> <!-- /lib/Horde/Kolab/FreeBusy -->
+      <dir name="Test">
+       <file name="FreeBusy.php" role="php" />
+      </dir> <!-- /lib/Horde/Kolab/Test -->
+     </dir> <!-- /lib/Horde/Kolab -->
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+   <dir name="test">
+    <dir name="Horde">
+     <dir name="Kolab">
+      <dir name="FreeBusy">
+       <file name="AllTests.php" role="test" />
+       <file name="FreeBusyTest.php" role="test" />
+       <file name="FreeBusyScenarioTest.php" role="test" />
+      </dir> <!-- /test/Horde/Kolab/FreeBusy -->
+     </dir> <!-- /test/Horde/Kolab -->
+    </dir> <!-- /test/Horde -->
+   </dir> <!-- /test -->
+   <dir name="www">
+    <dir name="Horde">
+     <dir name="Kolab">
+      <dir name="FreeBusy">
+       <file name="config.php" role="www" />
+       <file name="freebusy.php" role="www" />
+       <file name="pfb.php" role="www" />
+       <file name="regenerate.php" role="www" />
+      </dir> <!-- /www/Horde/Kolab/FreeBusy -->
+     </dir> <!-- /www/Horde/Kolab -->
+    </dir> <!-- /www/Horde -->
+   </dir> <!-- /www -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.0.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.7.0</min>
+   </pearinstaller>
+   <package>
+    <name>Kolab_Format</name>
+    <channel>pear.horde.org</channel>
+    <min>1.0.1</min>
+   </package>
+   <package>
+    <name>Kolab_Server</name>
+    <channel>pear.horde.org</channel>
+    <min>0.5.0</min>
+   </package>
+   <package>
+    <name>Kolab_Storage</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Horde_iCalendar</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Horde_Date</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Kolab/FreeBusy.php" as="Horde/Kolab/FreeBusy.php" />
+   <install name="lib/Horde/Kolab/FreeBusy/Access.php" as="Horde/Kolab/FreeBusy/Access.php" />
+   <install name="lib/Horde/Kolab/FreeBusy/Cache.php" as="Horde/Kolab/FreeBusy/Cache.php" />
+   <install name="lib/Horde/Kolab/FreeBusy/Imap.php" as="Horde/Kolab/FreeBusy/Imap.php" />
+   <install name="lib/Horde/Kolab/FreeBusy/Report.php" as="Horde/Kolab/FreeBusy/Report.php" />
+   <install name="lib/Horde/Kolab/FreeBusy/View.php" as="Horde/Kolab/FreeBusy/View.php" />
+   <install name="lib/Horde/Kolab/Test/FreeBusy.php" as="Horde/Kolab/Test/FreeBusy.php" />
+   <install name="test/Horde/Kolab/FreeBusy/AllTests.php" as="Horde/Kolab/FreeBusy/AllTests.php" />
+   <install name="test/Horde/Kolab/FreeBusy/FreeBusyTest.php" as="Horde/Kolab/FreeBusy/FreeBusyTest.php" />
+   <install name="test/Horde/Kolab/FreeBusy/FreeBusyScenarioTest.php" as="Horde/Kolab/FreeBusy/FreeBusyScenarioTest.php" />
+   <install name="www/Horde/Kolab/FreeBusy/config.php" as="config.php" />
+   <install name="www/Horde/Kolab/FreeBusy/freebusy.php" as="freebusy.php" />
+   <install name="www/Horde/Kolab/FreeBusy/pfb.php" as="pfb.php" />
+   <install name="www/Horde/Kolab/FreeBusy/regenerate.php" as="regenerate.php" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <date>2009-04-02</date>
+   <version>
+    <release>0.1.4</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+   <notes>
+    * kolab/issue3450 (freebusy/user@example.com.ifb returns extended
+      free/busy information)
+    * kolab/issue3527 (no immediately printing of folders during
+      freebusy cache regeneration)
+   </notes>
+  </release>
+  <release>
+   <date>2009-03-06</date>
+   <version>
+    <release>0.1.3</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+   <notes>
+     * SERVER_NAME undefined in freebusy/regenerate.php (kolab/issue3446,
+       https://www.intevation.de/roundup/kolab/issue3446)
+     * freebusy cache regeneration on the command line looks confusing
+       (kolab/issue3415, https://www.intevation.de/roundup/kolab/issue3415)
+     * kolabFreeBusyPast is not used (kolab/issue3438,
+       https://www.intevation.de/roundup/kolab/issue3438)
+     * Fixed manager access in the regeneration script
+       kolab/issue3313 (free/busy regeneration as manager broken in 2.2.1-beta1)
+   </notes>
+  </release>
+  <release>
+   <date>2008-12-12</date>
+   <version>
+    <release>0.1.2</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</license>
+   <notes>
+     * Fixed copyright information.
+   </notes>
+  </release>
+  <release>
+   <date>2008-12-05</date>
+   <version>
+    <release>0.1.1</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+    * Fixed handling of extended free/busy information.
+    * Fixed storing acl values when the folder is triggered by a foreign user 
+      kolab/issue3208 (Free/Busy list is always empty)
+    * Fixed identification of the corresponding free/busy server.
+    * Implemented extended free/busy access concept.
+    * kolab/issue3256 (resmgr responses should reflect server revision in PRODID)
+   </notes>
+  </release>
+  <release>
+   <date>2008-10-29</date>
+   <version>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+     * Initial release.
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/AllTests.php b/framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/AllTests.php
new file mode 100644 (file)
index 0000000..f1f4fff
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+/**
+ * All tests for the Kolab_FreeBusy:: package.
+ *
+ * $Horde: framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/AllTests.php,v 1.2 2009/01/06 17:49:24 jan Exp $
+ *
+ * @package Kolab_FreeBusy
+ */
+
+/**
+ * Define the main method 
+ */
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Horde_Kolab_FreeBusy_AllTests::main');
+}
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+/**
+ * Combine the tests for this package.
+ *
+ * $Horde: framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/AllTests.php,v 1.2 2009/01/06 17:49:24 jan Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @package Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_AllTests {
+
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Horde_Kolab_FreeBusy');
+
+        $basedir = dirname(__FILE__);
+        $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) {
+            if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+                $pathname = $file->getPathname();
+                require $pathname;
+
+                $class = str_replace(DIRECTORY_SEPARATOR, '_',
+                                     preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname));
+                $suite->addTestSuite('Horde_Kolab_FreeBusy_' . $class);
+            }
+        }
+
+        return $suite;
+    }
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Kolab_FreeBusy_AllTests::main') {
+    Horde_Kolab_FreeBusy_AllTests::main();
+}
diff --git a/framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/FreeBusyScenarioTest.php b/framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/FreeBusyScenarioTest.php
new file mode 100644 (file)
index 0000000..51b412f
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Checks for the Kolab Free/Busy system.
+ *
+ * $Horde: framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/FreeBusyScenarioTest.php,v 1.4 2009/04/25 19:30:35 wrobel Exp $
+ *
+ * PHP version 5
+ *
+ * @category   Horde
+ * @package    Share
+ * @subpackage UnitTests
+ * @author     Gunnar Wrobel <wrobel@pardus.de>
+ * @license    http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link       http://pear.horde.org/index.php?package=Share
+ */
+
+/**
+ *  We need the base class
+ */
+require_once 'Horde/Kolab/Test/FreeBusy.php';
+
+/**
+ * Checks for the Kolab Free/Busy system.
+ *
+ * $Horde: framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/FreeBusyScenarioTest.php,v 1.4 2009/04/25 19:30:35 wrobel Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Kolab
+ * @package  Kolab_Server
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_Server
+ */
+class Horde_Kolab_FreeBusy_FreeBusyScenarioTest extends Horde_Kolab_Test_FreeBusy
+{
+    /**
+     * Test triggering a calendar folder.
+     *
+     * @scenario
+     *
+     * @return NULL
+     */
+    public function triggering()
+    {
+        $this->given('a populated Kolab setup')
+            ->when('logging in as a user with a password', 'wrobel', 'none')
+            ->and('create a Kolab default calendar with name', 'Calendar')
+            ->and('triggering the folder', 'wrobel@example.org/Calendar')
+            ->then('the login was successful')
+            ->and('the creation of the folder was successful')
+            ->and('the result should be an object of type', 'Horde_Kolab_FreeBusy_View_vfb');
+    }
+
+    /**
+     * Test fetching free/busy data.
+     *
+     * @scenario
+     *
+     * @return NULL
+     */
+    public function fetching()
+    {
+        $now = time();
+        $event = array(
+            'uid' => 1,
+            'summary' => 'hello',
+            'start-date' => $now,
+            'end-date' => $now + 120,
+        );
+
+        $this->given('a populated Kolab setup')
+            ->when('logging in as a user with a password', 'wrobel', 'none')
+            ->and('create a Kolab default calendar with name', 'Calendar')
+            ->and('adding an event to a folder', $event, 'INBOX/Calendar')
+            ->and('triggering the folder', 'wrobel@example.org/Calendar')
+            ->and('fetching the free/busy information for', 'wrobel@example.org')
+            ->then('the login was successful')
+            ->and('the creation of the folder was successful')
+            ->and('the fetch result should contain a free/busy time with summary', 'hello');
+    }
+
+    /**
+     * Test fetching free/busy data as a foreign user should not contain
+     * extended information.
+     *
+     * @scenario
+     *
+     * @return NULL
+     */
+    public function fetchingAsForeignUser()
+    {
+        $now = time();
+        $event = array(
+            'uid' => 1,
+            'summary' => 'hello',
+            'start-date' => $now,
+            'end-date' => $now + 120,
+        );
+
+        $this->given('a populated Kolab setup')
+            ->when('logging in as a user with a password', 'wrobel', 'none')
+            ->and('create a Kolab default calendar with name', 'Calendar')
+            ->and('adding an event to a folder', $event, 'INBOX/Calendar')
+            ->and('triggering the folder', 'wrobel@example.org/Calendar')
+            ->and('logging in as a user with a password', 'test', 'test')
+            ->and('fetching the free/busy information for', 'wrobel@example.org')
+            ->then('the login was successful')
+            ->and('the creation of the folder was successful')
+            ->and('the fetch result should not contain a free/busy time with summary', 'hello');
+    }
+
+    /**
+     * Test fetching free/busy data as a foreign user in group with read access
+     * should contain extended information.
+     *
+     * @scenario
+     *
+     * @return NULL
+     */
+    public function fetchingAsForeignUserInSameGroup()
+    {
+        $now = time();
+        $event = array(
+            'uid' => 1,
+            'summary' => 'hello',
+            'start-date' => $now,
+            'end-date' => $now + 120,
+        );
+
+        $this->given('a populated Kolab setup')
+            ->when('logging in as a user with a password', 'wrobel', 'none')
+            ->and('create a Kolab default calendar with name', 'Calendar')
+            ->and('allow a group full access to a folder', 'group@example.org', 'INBOX/Calendar')
+            ->and('adding an event to a folder', $event, 'INBOX/Calendar')
+            ->and('triggering the folder', 'wrobel@example.org/Calendar')
+            ->and('logging in as a user with a password', 'test', 'test')
+            ->and('fetching the free/busy information for', 'wrobel@example.org')
+            ->then('the login was successful')
+            ->and('the creation of the folder was successful')
+            ->and('the fetch result should contain a free/busy time with summary', 'hello');
+    }
+}
\ No newline at end of file
diff --git a/framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/FreeBusyTest.php b/framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/FreeBusyTest.php
new file mode 100644 (file)
index 0000000..98ad0f6
--- /dev/null
@@ -0,0 +1,181 @@
+<?php
+/**
+ * Test the Kolab free/busy system.
+ *
+ * $Horde: framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/FreeBusyTest.php,v 1.13 2009/06/09 23:23:38 slusarz Exp $
+ *
+ * @category Kolab
+ * @package  Kolab_FreeBusy
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_FreeBusy
+ */
+
+/**
+ *  We need the base class
+ */
+require_once 'Horde/Kolab/Test/Storage.php';
+
+require_once 'Horde/Kolab/FreeBusy.php';
+
+/**
+ * Test the Kolab free/busy system.
+ *
+ * $Horde: framework/Kolab_FreeBusy/test/Horde/Kolab/FreeBusy/FreeBusyTest.php,v 1.13 2009/06/09 23:23:38 slusarz Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ * 
+ * @category Kolab
+ * @package  Kolab_FreeBusy
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Kolab_FreeBusy
+ */
+class Horde_Kolab_FreeBusy_FreeBusyTest extends Horde_Kolab_Test_FreeBusy
+{
+
+    /**
+     * Test setup.
+     */
+    public function setUp()
+    {
+        $world = $this->prepareBasicSetup();
+
+        global $conf;
+        $conf['kolab']['ldap']['phpdn'] = null;
+        $conf['fb']['cache_dir']             = '/tmp';
+        $conf['kolab']['freebusy']['server'] = 'https://fb.example.org/freebusy';
+        $conf['fb']['use_acls'] = true;
+
+        $this->assertTrue($world['auth']->authenticate('wrobel@example.org',
+                                                        array('password' => 'none')));
+
+        $folder = $world['storage']->getNewFolder();
+        $folder->setName('Calendar');
+        $this->assertNoError($folder->save(array('type' => 'event',
+                                                 'default' => true)));
+
+        $this->server = $world['server'];
+        $this->auth = $world['auth'];
+    }
+
+    /**
+     * Add an event.
+     *
+     * @return NULL
+     */
+    public function _addEvent($start)
+    {
+        include_once 'Horde/Kolab/Storage.php';
+
+        $folder = Kolab_Storage::getShare('INBOX/Calendar', 'event');
+        $data   = Kolab_Storage::getData($folder, 'event', 1);
+        $object = array(
+            'uid' => 1,
+            'summary' => 'test',
+            'start-date' => $start,
+            'end-date' => $start + 120,
+        );
+
+        /* Add the event */
+        $result = $data->save($object);
+        if (is_a($result, 'PEAR_Error')) {
+            $this->assertEquals('', $result->getMessage());
+        }
+    }
+
+    /**
+     * Test getting free/busy information.
+     *
+     * @return NULL
+     */
+    public function testFetch()
+    {
+        $start = time();
+
+        $this->_addEvent($start);
+
+        $_GET['folder'] = 'wrobel@example.org/Calendar';
+
+        $fb = &new Horde_Kolab_FreeBusy();
+
+        /** Trigger the free/busy cache update */
+        $view = $fb->trigger();
+        $this->assertTrue(is_a($view, 'Horde_Kolab_FreeBusy_View_vfb'));
+
+        $vcal = $view->_data['fb'];
+
+        $vfb = $vcal->findComponent('VFREEBUSY');
+        $p = $vfb->getBusyPeriods();
+
+        $this->assertTrue($p[$start] == $start + 120);
+   }
+
+    /**
+     * Test triggering.
+     *
+     * @return NULL
+     */
+    public function testTrigger()
+    {
+        $_GET['folder'] = 'wrobel@example.org/Calendar';
+        $_GET['extended'] = '1';
+
+        $req_folder = Horde_Util::getFormData('folder', '');
+        $access = &new Horde_Kolab_FreeBusy_Access();
+        $result = $access->parseFolder($req_folder);
+        $this->assertEquals('wrobel@example.org', $access->owner);
+
+        $result = $this->server->uidForIdOrMailOrAlias($access->owner);
+        $this->assertEquals('cn=Gunnar Wrobel,dc=example,dc=org', $result);
+
+        $result = $this->server->fetch($result, 'Horde_Kolab_Server_Object_Kolab_User');
+        $this->assertNoError($result);
+
+        $fb = &new Horde_Kolab_FreeBusy();
+        $view = $fb->trigger();
+        $this->assertEquals('Horde_Kolab_FreeBusy_View_vfb', get_class($view));
+
+        /** Test triggering an invalid folder */
+        $_GET['folder'] = '';
+
+        $fb = &new Horde_Kolab_FreeBusy();
+
+        /** Trigger the free/busy cache update */
+        $view = $fb->trigger();
+        $this->assertTrue(is_a($view, 'Horde_Kolab_FreeBusy_View_error'));
+        $this->assertEquals('No such folder ', $view->_data['error']->getMessage());
+    }
+
+    /**
+     * Test triggering the folder of another user.
+     *
+     * @return NULL
+     */
+    public function testForeignTrigger()
+    {
+        $start = time();
+
+        $this->_addEvent($start);
+
+        $this->assertTrue($this->auth->authenticate('test@example.org',
+                                                        array('password' => 'test')));
+
+        $_GET['folder'] = 'wrobel@example.org/Calendar';
+        $_GET['extended'] = '1';
+
+        $fb = &new Horde_Kolab_FreeBusy();
+        $view = $fb->trigger();
+        $this->assertEquals('Horde_Kolab_FreeBusy_View_vfb', get_class($view));
+
+        $vcal = $view->_data['fb'];
+
+        $vfb = $vcal->findComponent('VFREEBUSY');
+        $p = $vfb->getBusyPeriods();
+
+        $this->assertTrue($p[$start] == $start + 120);
+    }
+}
diff --git a/framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/config.php b/framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/config.php
new file mode 100644 (file)
index 0000000..8c83854
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+/**
+ * This file provides configuration settings for both the freebusy.php
+ * and the pfb.php scripts.
+ *
+ * $Horde: framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/config.php,v 1.4 2009/01/06 17:49:24 jan Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Thomas Arendsen Hein <thomas@intevation.de>
+ * @package Kolab_FreeBusy
+ */
+
+$conf = array();
+
+/* Horde::Log configuration */
+$conf['log']['enabled']          = true;
+$conf['log']['priority']         = PEAR_LOG_DEBUG; // Leave this on DEBUG for now. We just restructured the package...
+$conf['log']['type']             = 'file';
+$conf['log']['name']             = '/kolab/var/kolab-freebusy/log/freebusy.log';
+$conf['log']['ident']            = 'Kolab Free/Busy';
+$conf['log']['params']['append'] = true;
+
+/* PHP error logging */
+ini_set('error_log', '/kolab/var/kolab-freebusy/log/php-error.log');
+
+/* Horde::Kolab::LDAP configuration */
+$conf['kolab']['ldap']['server'] = 'example.com';
+$conf['kolab']['ldap']['basedn'] = 'dc=example,dc=com';
+$conf['kolab']['ldap']['phpdn']  = 'cn=nobody,cn=internal,dc=example,dc=com';
+$conf['kolab']['ldap']['phppw']  = 'xyz';
+
+/* Horde::Kolab::IMAP configuration */
+$conf['kolab']['imap']['server']   = 'example.com';
+$conf['kolab']['imap']['port']     = 143;
+$conf['kolab']['imap']['protocol'] = 'notls/readonly';
+
+/* Horde::Auth configuration */
+$conf['auth']['params']['login_block'] = 0;
+$conf['auth']['checkbrowser']          = false;
+$conf['auth']['checkip']               = false;
+
+/* Kolab::Freebusy configuration */
+
+/* Should we redirect using a Location header, if the user is not local? If this
+ * is false we silently download the file ourselves and output it so that it
+ * looks as though the free/busy information is coming from us.
+ */
+$conf['fb']['redirect']     = false;
+
+/* What is the address of the current server where the calendar data is stored?
+ * This is also used as the LDAP server address where user objects reside.
+ */
+$conf['kolab']['freebusy']['server']  = 'https://example.com/freebusy';
+
+/* What is our default mail domain? This is used if any users do not have
+ * '@domain' specified after their username as part of their email address.
+ */
+$conf['fb']['email_domain'] = 'example.com';
+
+/* Location of the cache files */
+$conf['fb']['cache_dir']    = '/kolab/var/kolab-freebusy/cache';
+
+/* What db type to use for the freebusy caches */
+$conf['fb']['dbformat']     = 'db4';
+
+/* Should we send a Content-Type header, indicating what the mime type of the
+ * resulting VFB file is?
+ */
+$conf['fb']['send_content_type'] = false;
+
+/* Should we send a Content-Length header, indicating how large the resulting
+ * VFB file is?
+ */
+$conf['fb']['send_content_length'] = false;
+
+/* Should we send a Content-Disposition header, indicating what the name of the
+ * resulting VFB file should be?
+ */
+$conf['fb']['send_content_disposition'] = false;
+
+/* Should we use ACLs or does everybody get full rights? DO NOT set
+ * this to false if you don't know what you are doing. Your free/busy
+ * service should not be visible to any outside networks when
+ * disabling the use of ACL settings.
+ */
+$conf['fb']['use_acls'] = true;
+
+/* Are there remote servers on which users have additional (shared)
+ * folders? In that case free/busy information should also be fetched
+ * from these servers.
+ *
+ * Add them like this:
+ *
+ * array('remote1.example.com', 'remote2.example.com')
+ */
+$conf['fb']['remote_servers'] = array();
+
+//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+//
+// If you modify this file, please do not forget to also modify the
+// template in kolabd!
+//
+//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+// DEBUGGING
+// =========
+//
+// Activate this to see the log messages on the screen
+// $conf['log']['type'] = 'display';
+//
+// Activate this to see the php messages on the screen
+// ini_set('display_errors', 1);
+//
+// Both settings will disrupt header delivery (which should not cause a
+// problem).
diff --git a/framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/freebusy.php b/framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/freebusy.php
new file mode 100644 (file)
index 0000000..0e28802
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+/**
+ * A script for fetching the Kolab Free/Busy information.
+ *
+ * $Horde: framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/freebusy.php,v 1.6 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2009 Klarälvdalens Datakonsult AB
+ *
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Thomas Arendsen Hein <thomas@intevation.de>
+ * @package Kolab_FreeBusy
+ */
+
+/** Load the required free/busy library */
+require_once 'Horde/Kolab/FreeBusy.php';
+
+/** Load the configuration */
+require_once 'config.php';
+
+$fb = new Horde_Kolab_FreeBusy();
+$view = $fb->fetch();
+$view->render();
+
diff --git a/framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/pfb.php b/framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/pfb.php
new file mode 100644 (file)
index 0000000..4beb60b
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+/**
+ * A script for triggering an update of the Kolab Free/Busy information.
+ *
+ * This script generates partial free/busy information based on a
+ * single calendar folder on the Kolab groupware server. The partial
+ * information is cached and later assembled for display by the
+ * freebusy.php script.
+ *
+ * $Horde: framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/pfb.php,v 1.6 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2009 Klarälvdalens Datakonsult AB
+ *
+ * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Thomas Arendsen Hein <thomas@intevation.de>
+ * @package Kolab_FreeBusy
+ */
+
+/** Load the required free/busy library */
+require_once 'Horde/Kolab/FreeBusy.php';
+
+/** Load the configuration */
+require_once 'config.php';
+
+$fb = new Horde_Kolab_FreeBusy();
+$view = $fb->trigger();
+$view->render();
diff --git a/framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/regenerate.php b/framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/regenerate.php
new file mode 100644 (file)
index 0000000..a71904e
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+/**
+ * A script for regenerating the Kolab Free/Busy cache.
+ *
+ * $Horde: framework/Kolab_FreeBusy/www/Horde/Kolab/FreeBusy/regenerate.php,v 1.7 2009/07/14 00:28:33 mrubinsk Exp $
+ *
+ * Copyright 2004-2009 Klarälvdalens Datakonsult AB
+ *
+ * @author  Gunnar Wrobel <p@rdus.de>
+ * @author  Thomas Arendsen Hein <thomas@intevation.de>
+ * @package Kolab_FreeBusy
+ */
+
+/** Report all errors */
+error_reporting(E_ALL);
+
+/** requires safe_mode to be turned off */
+ini_set('memory_limit', -1);
+
+/**
+ * Load the required free/busy libraries - this also loads Horde:: and
+ * Horde_Util:: as well as the PEAR constants
+ */
+require_once 'Horde/Kolab/FreeBusy.php';
+require_once 'Horde/Kolab/FreeBusy/Report.php';
+
+/** Load the configuration */
+require_once 'config.php';
+
+$conf['kolab']['misc']['allow_special'] = true;
+
+if (empty($_SERVER['SERVER_NAME'])) {
+    $_SERVER['SERVER_NAME'] = $conf['kolab']['imap']['server'];
+}
+
+$fb_reporter = new Horde_Kolab_FreeBusy_Report();
+
+$fb_reporter->start();
+
+$fb = new Horde_Kolab_FreeBusy();
+$result = $fb->regenerate($fb_reporter);
+if (is_a($result, 'PEAR_Error')) {
+    echo $result->getMessage();
+    exit(1);
+}
+$result = $fb_reporter->stop();
+if ($result === false) {
+    exit(1);
+}
+exit(0);