Page MenuHomePhorge

No OneTemporary

Size
860 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/plugins/calendar/LICENSE b/plugins/calendar/LICENSE
new file mode 100644
index 00000000..dba13ed2
--- /dev/null
+++ b/plugins/calendar/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, 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
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state 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 program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index d498c8c1..e8293887 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1,2316 +1,2314 @@
<?php
/**
* Calendar plugin for Roundcube webmail
*
* @version 0.7-beta
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <roundcube@gmail.com>
*
- * Copyright (C) 2010, Lazlo Westerhof - Netherlands
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
class calendar extends rcube_plugin
{
const FREEBUSY_UNKNOWN = 0;
const FREEBUSY_FREE = 1;
const FREEBUSY_BUSY = 2;
const FREEBUSY_TENTATIVE = 3;
const FREEBUSY_OOF = 4;
public $task = '?(?!logout).*';
public $rc;
public $driver;
public $home; // declare public to be used in other classes
public $urlbase;
public $timezone;
public $gmt_offset;
public $ical;
public $ui;
public $defaults = array(
'calendar_default_view' => "agendaWeek",
'calendar_date_format' => "yyyy-MM-dd",
'calendar_date_short' => "M-d",
'calendar_date_long' => "MMM d yyyy",
'calendar_date_agenda' => "ddd MM-dd",
'calendar_time_format' => "HH:mm",
'calendar_timeslots' => 2,
'calendar_first_day' => 1,
'calendar_first_hour' => 6,
'calendar_work_start' => 6,
'calendar_work_end' => 18,
'calendar_agenda_range' => 60,
'calendar_agenda_sections' => 'smart',
'calendar_event_coloring' => 0,
'calendar_time_indicator' => true,
'calendar_date_format_sets' => array(
'yyyy-MM-dd' => array('MMM d yyyy', 'M-d', 'ddd MM-dd'),
'dd-MM-yyyy' => array('d MMM yyyy', 'd-M', 'ddd dd-MM'),
'yyyy/MM/dd' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
'MM/dd/yyyy' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
'dd/MM/yyyy' => array('d MMM yyyy', 'd/M', 'ddd dd/MM'),
'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M', 'ddd dd.MM.'),
'd.M.yyyy' => array('d. MMM yyyy', 'd.M', 'ddd d.MM.'),
),
);
private $default_categories = array(
'Personal' => 'c0c0c0',
'Work' => 'ff0000',
'Family' => '00ff00',
'Holiday' => 'ff6600',
);
private $ics_parts = array();
/**
* Plugin initialization.
*/
function init()
{
$this->rc = rcmail::get_instance();
$this->register_task('calendar', 'calendar');
// load calendar configuration
$this->load_config();
// load localizations
$this->add_texts('localization/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print'));
// set user's timezone
$this->timezone = $this->rc->config->get('timezone');
$this->dst_active = $this->rc->config->get('dst_active');
$this->gmt_offset = ($this->timezone + $this->dst_active) * 3600;
$this->user_timezone = new DateTimeZone($this->timezone ? timezone_name_from_abbr("", $this->gmt_offset, $this->dst_active) : 'GMT');
require($this->home . '/lib/calendar_ui.php');
$this->ui = new calendar_ui($this);
// load Calendar user interface which includes jquery-ui
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
$this->require_plugin('jqueryui');
$this->ui->init();
// settings are required in (almost) every GUI step
if ($this->rc->action != 'attend')
$this->rc->output->set_env('calendar_settings', $this->load_settings());
}
// catch iTIP confirmation requests that don're require a valid session
if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) {
$this->add_hook('startup', array($this, 'itip_attend_response'));
}
else if ($this->rc->task == 'calendar' && $this->rc->action != 'save-pref') {
if ($this->rc->action != 'upload') {
$this->load_driver();
}
// register calendar actions
$this->register_action('index', array($this, 'calendar_view'));
$this->register_action('event', array($this, 'event_action'));
$this->register_action('calendar', array($this, 'calendar_action'));
$this->register_action('load_events', array($this, 'load_events'));
$this->register_action('export_events', array($this, 'export_events'));
$this->register_action('import_events', array($this, 'import_events'));
$this->register_action('upload', array($this, 'attachment_upload'));
$this->register_action('get-attachment', array($this, 'attachment_get'));
$this->register_action('freebusy-status', array($this, 'freebusy_status'));
$this->register_action('freebusy-times', array($this, 'freebusy_times'));
$this->register_action('randomdata', array($this, 'generate_randomdata'));
$this->register_action('print', array($this,'print_view'));
$this->register_action('mailimportevent', array($this, 'mail_import_event'));
$this->register_action('mailtoevent', array($this, 'mail_message2event'));
$this->register_action('inlineui', array($this, 'get_inline_ui'));
$this->register_action('check-recent', array($this, 'check_recent'));
// remove undo information...
if ($undo = $_SESSION['calendar_event_undo']) {
// ...after timeout
$undo_time = $this->rc->config->get('undo_timeout', 0);
if ($undo['ts'] < time() - $undo_time) {
$this->rc->session->remove('calendar_event_undo');
// @TODO: do EXPUNGE on kolab objects?
}
}
}
else if ($this->rc->task == 'settings') {
// add hooks for Calendar settings
$this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list'));
$this->add_hook('preferences_list', array($this, 'preferences_list'));
$this->add_hook('preferences_save', array($this, 'preferences_save'));
}
else if ($this->rc->task == 'mail') {
// hooks to catch event invitations on incoming mails
if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
$this->add_hook('message_load', array($this, 'mail_message_load'));
$this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
}
// add 'Create event' item to message menu
if ($this->api->output->type == 'html') {
$this->api->add_content(html::tag('li', null,
$this->api->output->button(array(
'command' => 'calendar-create-from-mail',
'label' => 'calendar.createfrommail',
'type' => 'link',
'classact' => 'calendarlink active',
'class' => 'calendarlink',
))),
'messagemenu');
}
}
// add hook to display alarms
$this->add_hook('keep_alive', array($this, 'keep_alive'));
}
/**
* Helper method to load the backend driver according to local config
*/
private function load_driver()
{
if (is_object($this->driver))
return;
$driver_name = $this->rc->config->get('calendar_driver', 'database');
$driver_class = $driver_name . '_driver';
require_once($this->home . '/drivers/calendar_driver.php');
require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
switch ($driver_name) {
case "kolab":
$this->require_plugin('kolab_core');
default:
$this->driver = new $driver_class($this);
break;
}
}
/**
* Load iTIP functions
*/
private function load_itip()
{
if (!$this->itip) {
require_once($this->home . '/lib/calendar_itip.php');
$this->itip = new calendar_itip($this);
}
return $this->itip;
}
/**
* Load iCalendar functions
*/
public function get_ical()
{
if (!$this->ical) {
require_once($this->home . '/lib/calendar_ical.php');
$this->ical = new calendar_ical($this);
}
return $this->ical;
}
/**
* Render the main calendar view from skin template
*/
function calendar_view()
{
$this->rc->output->set_pagetitle($this->gettext('calendar'));
// Add CSS stylesheets to the page header
$this->ui->addCSS();
// Add JS files to the page header
$this->ui->addJS();
$this->ui->init_templates();
$this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning');
// initialize attendees autocompletion
rcube_autocomplete_init();
$this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
$this->rc->output->set_env('mscolors', $this->driver->get_color_values());
$view = get_input_value('view', RCUBE_INPUT_GPC);
if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
$this->rc->output->set_env('view', $view);
if ($date = get_input_value('date', RCUBE_INPUT_GPC))
$this->rc->output->set_env('date', $date);
$this->rc->output->send("calendar.calendar");
}
/**
* Handler for preferences_sections_list hook.
* Adds Calendar settings sections into preferences sections list.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_sections_list($p)
{
$p['list']['calendar'] = array(
'id' => 'calendar', 'section' => $this->gettext('calendar'),
);
return $p;
}
/**
* Handler for preferences_list hook.
* Adds options blocks into Calendar settings sections in Preferences.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_list($p)
{
if ($p['section'] == 'calendar') {
$this->load_driver();
$p['blocks']['view']['name'] = $this->gettext('mainoptions');
$field_id = 'rcmfd_default_view';
$select = new html_select(array('name' => '_default_view', 'id' => $field_id));
$select->add($this->gettext('day'), "agendaDay");
$select->add($this->gettext('week'), "agendaWeek");
$select->add($this->gettext('month'), "month");
$select->add($this->gettext('agenda'), "table");
$p['blocks']['view']['options']['default_view'] = array(
'title' => html::label($field_id, Q($this->gettext('default_view'))),
'content' => $select->show($this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view'])),
);
$field_id = 'rcmfd_timeslot';
$choices = array('1', '2', '3', '4', '6');
$select = new html_select(array('name' => '_timeslots', 'id' => $field_id));
$select->add($choices);
$p['blocks']['view']['options']['timeslots'] = array(
'title' => html::label($field_id, Q($this->gettext('timeslots'))),
'content' => $select->show($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots'])),
);
$field_id = 'rcmfd_firstday';
$select = new html_select(array('name' => '_first_day', 'id' => $field_id));
$select->add(rcube_label('sunday'), '0');
$select->add(rcube_label('monday'), '1');
$select->add(rcube_label('tuesday'), '2');
$select->add(rcube_label('wednesday'), '3');
$select->add(rcube_label('thursday'), '4');
$select->add(rcube_label('friday'), '5');
$select->add(rcube_label('saturday'), '6');
$p['blocks']['view']['options']['first_day'] = array(
'title' => html::label($field_id, Q($this->gettext('first_day'))),
'content' => $select->show($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day'])),
);
$time_format = $this->rc->config->get('time_format', self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format'])));
$select_hours = new html_select();
for ($h = 0; $h < 24; $h++)
$select_hours->add(date($time_format, mktime($h, 0, 0)), $h);
$field_id = 'rcmfd_firsthour';
$p['blocks']['view']['options']['first_hour'] = array(
'title' => html::label($field_id, Q($this->gettext('first_hour'))),
'content' => $select_hours->show($this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)),
);
$field_id = 'rcmfd_workstart';
$p['blocks']['view']['options']['workinghours'] = array(
'title' => html::label($field_id, Q($this->gettext('workinghours'))),
'content' => $select_hours->show($this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) .
' &mdash; ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)),
);
$field_id = 'rcmfd_coloing';
$select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id));
$select_colors->add($this->gettext('coloringmode0'), 0);
$select_colors->add($this->gettext('coloringmode1'), 1);
$select_colors->add($this->gettext('coloringmode2'), 2);
$select_colors->add($this->gettext('coloringmode3'), 3);
$p['blocks']['view']['options']['eventcolors'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('eventcoloring'))),
'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])),
);
$field_id = 'rcmfd_alarm';
$select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id));
$select_type->add($this->gettext('none'), '');
foreach ($this->driver->alarm_types as $type)
$select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
$input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3));
$select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset'));
foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
$select_offset->add($this->gettext('trigger' . $trigger), $trigger);
$p['blocks']['view']['options']['alarmtype'] = array(
'title' => html::label($field_id, Q($this->gettext('defaultalarmtype'))),
'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')),
);
$preset = self::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
$p['blocks']['view']['options']['alarmoffset'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))),
'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]),
);
// default calendar selection
$field_id = 'rcmfd_default_calendar';
$select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id));
foreach ((array)$this->driver->list_calendars() as $id => $prop) {
if (!$prop['readonly'])
$select_cal->add($prop['name'], strval($id));
}
$p['blocks']['view']['options']['defaultcalendar'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('defaultcalendar'))),
'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', '')),
);
// category definitions
if (!$this->driver->nocategories) {
$p['blocks']['categories']['name'] = $this->gettext('categories');
$categories = (array) $this->driver->list_categories();
$categories_list = '';
foreach ($categories as $name => $color) {
$key = md5($name);
$field_class = 'rcmfd_category_' . str_replace(' ', '_', $name);
$category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category')));
$category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30, 'disabled' => $this->driver->categoriesimmutable));
$category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6));
$hidden = $this->driver->categoriesimmutable ? html::tag('input', array('type' => 'hidden', 'name' => "_categories[$key]", 'value' => $name)) : '';
$categories_list .= html::div(null, $hidden . $category_name->show($name) . '&nbsp;' . $category_color->show($color) . '&nbsp;' . $category_remove->show());
}
$p['blocks']['categories']['options']['category_' . $name] = array(
'content' => html::div(array('id' => 'calendarcategories'), $categories_list),
);
$field_id = 'rcmfd_new_category';
$new_category = new html_inputfield(array('name' => '_new_category', 'id' => $field_id, 'size' => 30));
$add_category = new html_inputfield(array('type' => 'button', 'class' => 'button', 'value' => $this->gettext('add_category'), 'onclick' => "rcube_calendar_add_category()"));
$p['blocks']['categories']['options']['categories'] = array(
'content' => $new_category->show('') . '&nbsp;' . $add_category->show(),
);
$this->rc->output->add_script('function rcube_calendar_add_category(){
var name = $("#rcmfd_new_category").val();
if (name.length) {
var input = $("<input>").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name);
var color = $("<input>").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000");
var button = $("<input>").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() });
$("<div>").append(input).append("&nbsp;").append(color).append("&nbsp;").append(button).appendTo("#calendarcategories");
color.miniColors({ colorValues:mscolors });
}
}');
// include color picker
$this->include_script('lib/js/jquery.miniColors.min.js');
$this->include_stylesheet('skins/' .$this->rc->config->get('skin') . '/jquery.miniColors.css');
$this->rc->output->set_env('mscolors', $this->driver->get_color_values());
$this->rc->output->add_script('$("input.colors").miniColors({ colorValues:rcmail.env.mscolors })', 'docready');
}
}
return $p;
}
/**
* Handler for preferences_save hook.
* Executed on Calendar settings form submit.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_save($p)
{
if ($p['section'] == 'calendar') {
$this->load_driver();
// compose default alarm preset value
$alarm_offset = get_input_value('_alarm_offset', RCUBE_INPUT_POST);
$default_alam = $alarm_offset[0] . intval(get_input_value('_alarm_value', RCUBE_INPUT_POST)) . $alarm_offset[1];
$p['prefs'] = array(
'calendar_default_view' => get_input_value('_default_view', RCUBE_INPUT_POST),
'calendar_timeslots' => get_input_value('_timeslots', RCUBE_INPUT_POST),
'calendar_first_day' => get_input_value('_first_day', RCUBE_INPUT_POST),
'calendar_first_hour' => intval(get_input_value('_first_hour', RCUBE_INPUT_POST)),
'calendar_work_start' => intval(get_input_value('_work_start', RCUBE_INPUT_POST)),
'calendar_work_end' => intval(get_input_value('_work_end', RCUBE_INPUT_POST)),
'calendar_event_coloring' => intval(get_input_value('_event_coloring', RCUBE_INPUT_POST)),
'calendar_default_alarm_type' => get_input_value('_alarm_type', RCUBE_INPUT_POST),
'calendar_default_alarm_offset' => $default_alam,
'calendar_default_calendar' => get_input_value('_default_calendar', RCUBE_INPUT_POST),
'calendar_date_format' => null, // clear previously saved values
'calendar_time_format' => null,
);
// categories
if (!$this->driver->nocategories) {
$old_categories = $new_categories = array();
foreach ($this->driver->list_categories() as $name => $color) {
$old_categories[md5($name)] = $name;
}
$categories = get_input_value('_categories', RCUBE_INPUT_POST);
$colors = get_input_value('_colors', RCUBE_INPUT_POST);
foreach ($categories as $key => $name) {
$color = preg_replace('/^#/', '', strval($colors[$key]));
// rename categories in existing events -> driver's job
if ($oldname = $old_categories[$key]) {
$this->driver->replace_category($oldname, $name, $color);
unset($old_categories[$key]);
}
else
$this->driver->add_category($name, $color);
$new_categories[$name] = $color;
}
// these old categories have been removed, alter events accordingly -> driver's job
foreach ((array)$old_categories[$key] as $key => $name) {
$this->driver->remove_category($name);
}
$p['prefs']['calendar_categories'] = $new_categories;
}
}
return $p;
}
/**
* Dispatcher for calendar actions initiated by the client
*/
function calendar_action()
{
$action = get_input_value('action', RCUBE_INPUT_GPC);
$cal = get_input_value('c', RCUBE_INPUT_GPC);
$success = $reload = false;
if (isset($cal['showalarms']))
$cal['showalarms'] = intval($cal['showalarms']);
switch ($action) {
case "form-new":
case "form-edit":
echo $this->ui->calendar_editform($action, $cal);
exit;
case "new":
$success = $this->driver->create_calendar($cal);
$reload = true;
break;
case "edit":
$success = $this->driver->edit_calendar($cal);
$reload = true;
break;
case "remove":
if ($success = $this->driver->remove_calendar($cal))
$this->rc->output->command('plugin.destroy_source', array('id' => $cal['id']));
break;
case "subscribe":
if (!$this->driver->subscribe_calendar($cal))
$this->rc->output->show_message($this->gettext('errorsaving'), 'error');
return;
}
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else {
$error_msg = $this->gettext('errorsaving') . ($this->driver->last_error ? ': ' . $this->driver->last_error :'');
$this->rc->output->show_message($error_msg, 'error');
}
$this->rc->output->command('plugin.unlock_saving');
// TODO: keep view and date selection
if ($success && $reload)
$this->rc->output->redirect('');
}
/**
* Dispatcher for event actions initiated by the client
*/
function event_action()
{
$action = get_input_value('action', RCUBE_INPUT_GPC);
$event = get_input_value('e', RCUBE_INPUT_POST, true);
$success = $reload = $got_msg = false;
// don't notify if modifying a recurring instance (really?)
if ($event['_savemode'] && $event['_savemode'] != 'all' && $event['_notify'])
unset($event['_notify']);
// read old event data in order to find changes
if (($event['_notify'] || $event['decline']) && $action != 'new')
$old = $this->driver->get_event($event);
switch ($action) {
case "new":
// create UID for new event
$event['uid'] = $this->generate_uid();
$this->prepare_event($event, $action);
if ($success = $this->driver->new_event($event)) {
$event['id'] = $event['uid'];
$this->cleanup_event($event);
}
$reload = $success && $event['recurrence'] ? 2 : 1;
break;
case "edit":
$this->prepare_event($event, $action);
if ($success = $this->driver->edit_event($event))
$this->cleanup_event($event);
$reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
break;
case "resize":
$this->prepare_event($event, $action);
$success = $this->driver->resize_event($event);
$reload = $event['_savemode'] ? 2 : 1;
break;
case "move":
$this->prepare_event($event, $action);
$success = $this->driver->move_event($event);
$reload = $success && $event['_savemode'] ? 2 : 1;
break;
case "remove":
// remove previous deletes
$undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0;
$this->rc->session->remove('calendar_event_undo');
// search for event if only UID is given
if (!isset($event['calendar']) && $event['uid']) {
if (!($event = $this->driver->get_event($event, true))) {
break;
}
$undo_time = 0;
}
$success = $this->driver->remove_event($event, $undo_time < 1);
$reload = (!$success || $event['_savemode']) ? 2 : 1;
if ($undo_time > 0 && $success) {
$_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event);
// display message with Undo link.
$msg = html::span(null, $this->gettext('successremoval'))
. ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))",
JS_OBJECT_NAME, JS_OBJECT_NAME)), rcube_label('undo'));
$this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time);
$got_msg = true;
}
else if ($success) {
$this->rc->output->show_message('calendar.successremoval', 'confirmation');
$got_msg = true;
}
// send iTIP reply that participant has declined the event
if ($success && $event['decline']) {
$emails = $this->get_user_emails();
foreach ($old['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER')
$organizer = $attendee;
else if ($attendee['email'] && in_array($attendee['email'], $emails)) {
$old['attendees'][$i]['status'] = 'DECLINED';
}
}
$itip = $this->load_itip();
if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined'))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
break;
case "undo":
// Restore deleted event
$event = $_SESSION['calendar_event_undo']['data'];
if ($event)
$success = $this->driver->restore_event($event);
if ($success) {
$this->rc->session->remove('calendar_event_undo');
$this->rc->output->show_message('calendar.successrestore', 'confirmation');
$got_msg = true;
$reload = 2;
}
break;
case "rsvp-status":
$action = 'rsvp';
$status = $event['fallback'];
$html = html::div('rsvp-status', $status != 'CANCELLED' ? $this->gettext('acceptinvitation') : '');
$this->load_driver();
if ($existing = $this->driver->get_event($event, true)) {
$emails = $this->get_user_emails();
foreach ($existing['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array($attendee['email'], $emails)) {
$status = $attendee['status'];
break;
}
}
}
else {
// get a list of writeable calendars
$calendars = $this->driver->list_calendars();
$calendar_select = new html_select(array('name' => 'calendar', 'id' => 'calendar-saveto'));
$numcals = 0;
foreach ($calendars as $calendar) {
if (!$calendar['readonly']) {
$calendar_select->add($calendar['name'], $calendar['id']);
$numcals++;
}
}
if ($numcals <= 1)
$calendar_select = null;
}
if ($status == 'unknown') {
$html = html::div('rsvp-status', $this->gettext('notanattendee'));
$action = 'import';
}
else if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED'))) {
$html = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
if ($existing['changed'] && $event['changed'] < $existing['changed']) {
$action = '';
}
}
$this->rc->output->command('plugin.update_event_rsvp_status', array(
'uid' => $event['uid'],
'id' => asciiwords($event['uid'], true),
'status' => $status,
'action' => $action,
'html' => $html,
'select' => $calendar_select ? html::span('calendar-select', $this->gettext('saveincalendar') . '&nbsp;' . $calendar_select->show($this->rc->config->get('calendar_default_calendar'))) : '',
));
return;
case "rsvp":
$ev = $this->driver->get_event($event);
$ev['attendees'] = $event['attendees'];
$event = $ev;
if ($success = $this->driver->edit_event($event)) {
$status = get_input_value('status', RCUBE_INPUT_GPC);
$organizer = null;
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
break;
}
}
$itip = $this->load_itip();
if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
break;
case "dismiss":
foreach (explode(',', $event['id']) as $id)
$success |= $this->driver->dismiss_alarm($id, $event['snooze']);
break;
}
// send out notifications
if ($success && $event['_notify'] && ($event['attendees'] || $old['attendees'])) {
// make sure we have the complete record
$event = $action == 'remove' ? $old : $this->driver->get_event($event);
// only notify if data really changed (TODO: do diff check on client already)
if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
if ($this->notify_attendees($event, $old, $action) < 0)
$this->rc->output->show_message('calendar.errornotifying', 'error');
}
}
// show confirmation/error message
if (!$got_msg) {
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('calendar.errorsaving', 'error');
}
// unlock client
$this->rc->output->command('plugin.unlock_saving');
// update event object on the client or trigger a complete refretch if too complicated
if ($reload) {
$args = array('source' => $event['calendar']);
if ($reload > 1)
$args['refetch'] = true;
else if ($success && $action != 'remove')
$args['update'] = $this->_client_event($this->driver->get_event($event));
$this->rc->output->command('plugin.refresh_calendar', $args);
}
}
/**
* Handler for load-requests from fullcalendar
* This will return pure JSON formatted output
*/
function load_events()
{
$events = $this->driver->load_events(
get_input_value('start', RCUBE_INPUT_GET),
get_input_value('end', RCUBE_INPUT_GET),
($query = get_input_value('q', RCUBE_INPUT_GET)),
get_input_value('source', RCUBE_INPUT_GET)
);
echo $this->encode($events, !empty($query));
exit;
}
/**
* Handler for keep-alive requests
* This will check for pending notifications and pass them to the client
*/
function keep_alive($attr)
{
$this->load_driver();
$alarms = $this->driver->pending_alarms(time());
if ($alarms) {
// make sure texts and env vars are available on client
if ($this->rc->task != 'calendar') {
$this->add_texts('localization/', true);
$this->rc->output->set_env('snooze_select', $this->ui->snooze_select());
}
$this->rc->output->command('plugin.display_alarms', $this->_alarms_output($alarms));
}
}
/**
* Handler for check-recent requests which are accidentally sent to calendar taks
*/
function check_recent()
{
// NOP
$this->rc->output->send();
}
/**
*
*/
function import_events()
{
// Upload progress update
if (!empty($_GET['_progress'])) {
rcube_upload_progress();
}
$calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
$uploadid = get_input_value('_uploadid', RCUBE_INPUT_GPC);
// process uploaded file if there is no error
$err = $_FILES['_data']['error'];
if (!$err && $_FILES['_data']['tmp_name']) {
$calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
$events = $this->get_ical()->import_from_file($_FILES['_data']['tmp_name']);
$count = $errors = 0;
$rangestart = $_REQUEST['_range'] ? strtotime("now -" . intval($_REQUEST['_range']) . " months") : 0;
foreach ($events as $event) {
// TODO: correctly handle recurring events which start before $rangestart
if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
continue;
$event['calendar'] = $calendar;
if ($success = $this->driver->new_event($event)) {
$count++;
}
else
$errors++;
}
if ($count) {
$this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation');
$this->rc->output->command('plugin.import_success', array('source' => $calendar, 'refetch' => true));
}
else if (!$errors) {
$this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');
$this->rc->output->command('plugin.import_success', array('source' => $calendar));
}
else
$this->rc->output->command('display_message', $this->gettext('importerror'), 'error');
}
else {
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
}
else {
$msg = rcube_label('fileuploaderror');
}
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('plugin.unlock_saving', false);
}
$this->rc->output->send('iframe');
}
/**
* Construct the ics file for exporting events to iCalendar format;
*/
function export_events()
{
$start = get_input_value('start', RCUBE_INPUT_GET);
$end = get_input_value('end', RCUBE_INPUT_GET);
if (!$start) $start = mktime(0, 0, 0, 1, date('n'), date('Y')-1);
if (!$end) $end = mktime(0, 0, 0, 31, 12, date('Y')+10);
$calid = $calname = get_input_value('source', RCUBE_INPUT_GET);
$calendars = $this->driver->list_calendars();
if ($calendars[$calid]) {
$calname = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
$events = $this->driver->load_events($start, $end, null, $calid, 0);
}
header("Content-Type: text/calendar");
header("Content-Disposition: inline; filename=".$calname.'.ics');
$this->get_ical()->export($events, '', true);
exit;
}
/**
*
*/
function load_settings()
{
$this->date_format_defaults();
$settings = array();
// configuration
$settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar');
$settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']);
$settings['date_format'] = (string)$this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']);
$settings['time_format'] = (string)$this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']);
$settings['date_short'] = (string)$this->rc->config->get('calendar_date_short', $this->defaults['calendar_date_short']);
$settings['date_long'] = (string)$this->rc->config->get('calendar_date_long', $this->defaults['calendar_date_long']);
$settings['dates_long'] = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '&mdash;' " . $settings['date_long'] . '}';
$settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']);
$settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);
$settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
$settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);
$settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']);
$settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']);
$settings['agenda_range'] = (int)$this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']);
$settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']);
$settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
$settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']);
$settings['timezone'] = $this->timezone;
$settings['dst'] = $this->dst_active;
// localization
$settings['days'] = array(
rcube_label('sunday'), rcube_label('monday'),
rcube_label('tuesday'), rcube_label('wednesday'),
rcube_label('thursday'), rcube_label('friday'),
rcube_label('saturday')
);
$settings['days_short'] = array(
rcube_label('sun'), rcube_label('mon'),
rcube_label('tue'), rcube_label('wed'),
rcube_label('thu'), rcube_label('fri'),
rcube_label('sat')
);
$settings['months'] = array(
$this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
$this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
$this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
$this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
$this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
$this->rc->gettext('longnov'), $this->rc->gettext('longdec')
);
$settings['months_short'] = array(
$this->rc->gettext('jan'), $this->rc->gettext('feb'),
$this->rc->gettext('mar'), $this->rc->gettext('apr'),
$this->rc->gettext('may'), $this->rc->gettext('jun'),
$this->rc->gettext('jul'), $this->rc->gettext('aug'),
$this->rc->gettext('sep'), $this->rc->gettext('oct'),
$this->rc->gettext('nov'), $this->rc->gettext('dec')
);
$settings['today'] = $this->rc->gettext('today');
// get user identity to create default attendee
if ($this->ui->screen == 'calendar') {
foreach ($this->rc->user->list_identities() as $rec) {
if (!$identity)
$identity = $rec;
$identity['emails'][] = $rec['email'];
}
$identity['emails'][] = $this->rc->user->get_username();
$settings['identity'] = array('name' => $identity['name'], 'email' => $identity['email'], 'emails' => ';' . join(';', $identity['emails']));
}
return $settings;
}
/**
* Helper function to set date/time format according to config and user preferences
*/
private function date_format_defaults()
{
static $defaults = array();
// nothing to be done
if (isset($defaults['date_format']))
return;
$defaults['date_format'] = $this->rc->config->get('calendar_date_format', self::from_php_date_format($this->rc->config->get('date_format')));
$defaults['time_format'] = $this->rc->config->get('calendar_time_format', self::from_php_date_format($this->rc->config->get('time_format')));
// override defaults
if ($defaults['date_format'])
$this->defaults['calendar_date_format'] = $defaults['date_format'];
if ($defaults['time_format'])
$this->defaults['calendar_time_format'] = $defaults['time_format'];
// derive format variants from basic date format
$format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']);
if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) {
$this->defaults['calendar_date_long'] = $format_set[0];
$this->defaults['calendar_date_short'] = $format_set[1];
$this->defaults['calendar_date_agenda'] = $format_set[2];
}
}
/**
* Convert the given date string into a GMT-based time stamp
*/
function fromGMT($datetime)
{
$ts = is_numeric($datetime) ? $datetime : strtotime($datetime);
return $ts + $this->gmt_offset;
}
/**
* Encode events as JSON
*
* @param array Events as array
* @param boolean Add CSS class names according to calendar and categories
* @return string JSON encoded events
*/
function encode($events, $addcss = false)
{
$json = array();
foreach ($events as $event) {
$json[] = $this->_client_event($event, $addcss);
}
return json_encode($json);
}
/**
* Convert an event object to be used on the client
*/
private function _client_event($event, $addcss = false)
{
// compose a human readable strings for alarms_text and recurrence_text
if ($event['alarms'])
$event['alarms_text'] = $this->_alarms_text($event['alarms']);
if ($event['recurrence'])
$event['recurrence_text'] = $this->_recurrence_text($event['recurrence']);
return array(
'start' => gmdate('c', $this->fromGMT($event['start'])), // client treats date strings as they were in users's timezone
'end' => gmdate('c', $this->fromGMT($event['end'])), // so shift timestamps to users's timezone and render a date string
'description' => strval($event['description']),
'location' => strval($event['location']),
'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') . 'fc-event-cat-' . asciiwords(strtolower($event['categories']), true),
'allDay' => ($event['allday'] == 1),
) + $event;
}
/**
* Generate reduced and streamlined output for pending alarms
*/
private function _alarms_output($alarms)
{
$out = array();
foreach ($alarms as $alarm) {
$out[] = array(
'id' => $alarm['id'],
'start' => gmdate('c', $this->fromGMT($alarm['start'])),
'end' => gmdate('c', $this->fromGMT($alarm['end'])),
'allDay' => ($event['allday'] == 1)?true:false,
'title' => $alarm['title'],
'location' => $alarm['location'],
'calendar' => $alarm['calendar'],
);
}
return $out;
}
/**
* Render localized text for alarm settings
*/
private function _alarms_text($alarm)
{
list($trigger, $action) = explode(':', $alarm);
$text = '';
switch ($action) {
case 'EMAIL':
$text = $this->gettext('alarmemail');
break;
case 'DISPLAY':
$text = $this->gettext('alarmdisplay');
break;
}
if (preg_match('/@(\d+)/', $trigger, $m)) {
$text .= ' ' . $this->gettext(array('name' => 'alarmat', 'vars' => array('datetime' => format_date($m[1]))));
}
else if ($val = self::parse_alaram_value($trigger)) {
$text .= ' ' . intval($val[0]) . ' ' . $this->gettext('trigger' . $val[1]);
}
else
return false;
return $text;
}
/**
* Render localized text describing the recurrence rule of an event
*/
private function _recurrence_text($rrule)
{
// TODO: finish this
$freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']);
$details = '';
switch ($rrule['FREQ']) {
case 'DAILY':
$freq .= $this->gettext('days');
break;
case 'WEEKLY':
$freq .= $this->gettext('weeks');
break;
case 'MONTHLY':
$freq .= $this->gettext('months');
break;
case 'YEARY':
$freq .= $this->gettext('years');
break;
}
if ($rrule['INTERVAL'] <= 1)
$freq = $this->gettext(strtolower($rrule['FREQ']));
if ($rrule['COUNT'])
$until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
else if ($rrule['UNTIL'])
$until = $this->gettext('recurrencend') . ' ' . format_date($rrule['UNTIL'], self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])));
else
$until = $this->gettext('forever');
return rtrim($freq . $details . ', ' . $until);
}
/**
* Generate a unique identifier for an event
*/
public function generate_uid()
{
return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
}
/**
* Helper function to convert alarm trigger strings
* into two-field values (e.g. "-45M" => 45, "-M")
*/
public static function parse_alaram_value($val)
{
if ($val[0] == '@')
return array(substr($val, 1));
else if (preg_match('/([+-])(\d+)([HMD])/', $val, $m))
return array($m[2], $m[1].$m[3]);
return false;
}
/**
* Convert the internal structured data into a vcalendar rrule 2.0 string
*/
public static function to_rrule($recurrence)
{
if (is_string($recurrence))
return $recurrence;
$rrule = '';
foreach ((array)$recurrence as $k => $val) {
$k = strtoupper($k);
switch ($k) {
case 'UNTIL':
$val = gmdate('Ymd\THis', $val);
break;
case 'EXDATE':
foreach ((array)$val as $i => $ex)
$val[$i] = gmdate('Ymd\THis', $ex);
$val = join(',', $val);
break;
}
$rrule .= $k . '=' . $val . ';';
}
return $rrule;
}
/**
* Convert from fullcalendar date format to PHP date() format string
*/
private static function to_php_date_format($from)
{
// "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s"
return strtr(strtr($from, array(
'yyyy' => 'Y',
'yy' => 'y',
'MMMM' => 'F',
'MMM' => 'M',
'MM' => 'm',
'M' => 'n',
'dddd' => 'l',
'ddd' => 'D',
'dd' => 'd',
'HH' => '**',
'hh' => '%%',
'H' => 'G',
'h' => 'g',
'mm' => 'i',
'ss' => 's',
'TT' => 'A',
'tt' => 'a',
'T' => 'A',
't' => 'a',
'u' => 'c',
)), array(
'**' => 'H',
'%%' => 'h',
));
}
/**
* Convert from PHP date() format to fullcalendar format string
*/
private static function from_php_date_format($from)
{
// "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss"
return strtr($from, array(
'y' => 'yy',
'Y' => 'yyyy',
'M' => 'MMM',
'F' => 'MMMM',
'm' => 'MM',
'n' => 'M',
'd' => 'dd',
'D' => 'ddd',
'l' => 'dddd',
'H' => 'HH',
'h' => 'hh',
'G' => 'H',
'g' => 'h',
'i' => 'mm',
's' => 'ss',
'A' => 'TT',
'a' => 'tt',
'c' => 'u',
));
}
/**
* TEMPORARY: generate random event data for testing
* Create events by opening http://<roundcubeurl>/?_task=calendar&_action=randomdata&_num=500
*/
public function generate_randomdata()
{
$num = $_REQUEST['_num'] ? intval($_REQUEST['_num']) : 100;
$cats = array_keys($this->driver->list_categories());
$cals = array();
foreach ($this->driver->list_calendars() as $cid => $cal) {
if ($cal['active'])
$cals[$cid] = $cal;
}
while ($count++ < $num) {
$start = round((time() + rand(-2600, 2600) * 1000) / 300) * 300;
$duration = round(rand(30, 360) / 30) * 30 * 60;
$allday = rand(0,20) > 18;
$alarm = rand(-30,12) * 5;
$fb = rand(0,2);
if (date('G', $start) > 23)
$start -= 3600;
if ($allday) {
$start = strtotime(date('Y-m-d 00:00:00', $start));
$duration = 86399;
}
$title = '';
$len = rand(2, 12);
$words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a feature(s) is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform (hereafter referred to without the classical prefix ) retains many applications, as most manufac- tured parts (and many anatomical parts investigated in medical imagery) contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise.");
$chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890";
for ($i = 0; $i < $len; $i++)
$title .= $words[rand(0,count($words)-1)] . " ";
$this->driver->new_event(array(
'uid' => $this->generate_uid(),
'start' => $start,
'end' => $start + $duration,
'allday' => $allday,
'title' => rtrim($title),
'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'),
'categories' => $cats[array_rand($cats)],
'calendar' => array_rand($cals),
'alarms' => $alarm > 0 ? "-{$alarm}M:DISPLAY" : '',
'priority' => rand(0,9),
));
}
$this->rc->output->redirect('');
}
/**
* Handler for attachments upload
*/
public function attachment_upload()
{
// Upload progress update
if (!empty($_GET['_progress'])) {
rcube_upload_progress();
}
$event = get_input_value('_id', RCUBE_INPUT_GPC);
$calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
$uploadid = get_input_value('_uploadid', RCUBE_INPUT_GPC);
$eventid = 'cal:'.$event;
if (!is_array($_SESSION['event_session']) || $_SESSION['event_session']['id'] != $eventid) {
$_SESSION['event_session'] = array();
$_SESSION['event_session']['id'] = $eventid;
$_SESSION['event_session']['attachments'] = array();
}
// clear all stored output properties (like scripts and env vars)
$this->rc->output->reset();
if (is_array($_FILES['_attachments']['tmp_name'])) {
foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
// Process uploaded attachment if there is no error
$err = $_FILES['_attachments']['error'][$i];
if (!$err) {
$attachment = array(
'path' => $filepath,
'size' => $_FILES['_attachments']['size'][$i],
'name' => $_FILES['_attachments']['name'][$i],
'mimetype' => rc_mime_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]),
'group' => $eventid,
);
$attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment);
}
if (!$err && $attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
// store new attachment in session
unset($attachment['status'], $attachment['abort']);
$_SESSION['event_session']['attachments'][$id] = $attachment;
if (($icon = $_SESSION['calendar_deleteicon']) && is_file($icon)) {
$button = html::img(array(
'src' => $icon,
'alt' => rcube_label('delete')
));
}
else {
$button = Q(rcube_label('delete'));
}
$content = html::a(array(
'href' => "#delete",
'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", JS_OBJECT_NAME, $id),
'title' => rcube_label('delete'),
), $button);
$content .= Q($attachment['name']);
$this->rc->output->command('add2attachment_list', "rcmfile$id", array(
'html' => $content,
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'complete' => true), $uploadid);
}
else { // upload failed
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
}
else if ($attachment['error']) {
$msg = $attachment['error'];
}
else {
$msg = rcube_label('fileuploaderror');
}
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('remove_from_attachment_list', $uploadid);
}
}
}
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// if filesize exceeds post_max_size then $_FILES array is empty,
// show filesizeerror instead of fileuploaderror
if ($maxsize = ini_get('post_max_size'))
$msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
'size' => show_bytes(parse_bytes($maxsize)))));
else
$msg = rcube_label('fileuploaderror');
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('remove_from_attachment_list', $uploadid);
}
$this->rc->output->send('iframe');
}
/**
* Handler for attachments download/displaying
*/
public function attachment_get()
{
$event = get_input_value('_event', RCUBE_INPUT_GPC);
$calendar = get_input_value('_cal', RCUBE_INPUT_GPC);
$id = get_input_value('_id', RCUBE_INPUT_GPC);
$event = array('id' => $event, 'calendar' => $calendar);
// show loading page
if (!empty($_GET['_preload'])) {
$url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
$message = rcube_label('loadingdata');
header('Content-Type: text/html; charset=' . RCMAIL_CHARSET);
print "<html>\n<head>\n"
. '<meta http-equiv="refresh" content="0; url='.Q($url).'">' . "\n"
. '<meta http-equiv="content-type" content="text/html; charset='.RCMAIL_CHARSET.'">' . "\n"
. "</head>\n<body>\n$message\n</body>\n</html>";
exit;
}
ob_end_clean();
send_nocacheing_headers();
if (isset($_SESSION['calendar_attachment']))
$attachment = $_SESSION['calendar_attachment'];
else
$attachment = $_SESSION['calendar_attachment'] = $this->driver->get_attachment($id, $event);
// show part page
if (!empty($_GET['_frame'])) {
$this->attachment = $attachment;
$this->register_handler('plugin.attachmentframe', array($this, 'attachment_frame'));
$this->register_handler('plugin.attachmentcontrols', array($this->ui, 'attachment_controls'));
$this->rc->output->send('calendar.attachment');
exit;
}
$this->rc->session->remove('calendar_attachment');
if ($attachment) {
$mimetype = strtolower($attachment['mimetype']);
list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
$browser = $this->rc->output->browser;
// send download headers
if ($_GET['_download']) {
header("Content-Type: application/octet-stream");
if ($browser->ie)
header("Content-Type: application/force-download");
}
else if ($ctype_primary == 'text') {
header("Content-Type: text/$ctype_secondary");
}
else {
// $mimetype = rcmail_fix_mimetype($mimetype);
header("Content-Type: $mimetype");
header("Content-Transfer-Encoding: binary");
}
$body = $this->driver->get_attachment_body($id, $event);
// display page, @TODO: support text/plain (and maybe some other text formats)
if ($mimetype == 'text/html' && empty($_GET['_download'])) {
$OUTPUT = new rcube_html_page();
// @TODO: use washtml on $body
$OUTPUT->write($body);
}
else {
// don't kill the connection if download takes more than 30 sec.
@set_time_limit(0);
$filename = $attachment['name'];
$filename = preg_replace('[\r\n]', '', $filename);
if ($browser->ie && $browser->ver < 7)
$filename = rawurlencode(abbreviate_string($filename, 55));
else if ($browser->ie)
$filename = rawurlencode($filename);
else
$filename = addcslashes($filename, '"');
$disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
header("Content-Disposition: $disposition; filename=\"$filename\"");
echo $body;
}
exit;
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
}
/**
* Template object for attachment display frame
*/
public function attachment_frame($attrib)
{
$attachment = $_SESSION['calendar_attachment'];
$mimetype = strtolower($attachment['mimetype']);
list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
$attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
return html::iframe($attrib);
}
/**
* Prepares new/edited event properties before save
*/
private function prepare_event(&$event, $action)
{
$attachments = array();
$eventid = 'cal:'.$event['id'];
if (is_array($_SESSION['event_session']) && $_SESSION['event_session']['id'] == $eventid) {
if (!empty($_SESSION['event_session']['attachments'])) {
foreach ($_SESSION['event_session']['attachments'] as $id => $attachment) {
if (is_array($event['attachments']) && in_array($id, $event['attachments'])) {
$attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
}
}
}
}
$event['attachments'] = $attachments;
// check for organizer in attendees
if ($event['attendees'] && ($action == 'new' || $action == 'edit')) {
$emails = $this->get_user_emails();
$organizer = $owner = false;
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER')
$organizer = true;
if ($attendee['email'] == in_array($attendee['email'], $emails))
$owner = $i;
else if (!isset($attendee['rsvp']))
$event['attendees'][$i]['rsvp'] = true;
}
// set owner as organizer if yet missing
if (!$organizer && $owner !== false) {
$event['attendees'][$owner]['role'] = 'ORGANIZER';
unset($event['attendees'][$owner]['rsvp']);
}
else if (!$organizer && $action == 'new' && ($identity = $this->rc->user->get_identity()) && $identity['email']) {
array_unshift($event['attendees'], array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'], 'status' => 'ACCEPTED'));
}
}
}
/**
* Releases some resources after successful event save
*/
private function cleanup_event(&$event)
{
// remove temp. attachment files
$eventid = 'cal:'.$event['id'];
if (!empty($_SESSION['event_session']) && ($eventid = $_SESSION['event_session']['id'])) {
$this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $eventid));
unset($_SESSION['event_session']);
}
}
/**
* Send out an invitation/notification to all event attendees
*/
private function notify_attendees($event, $old, $action = 'edit')
{
if ($action == 'remove') {
$event['cancelled'] = true;
$is_cancelled = true;
}
$itip = $this->load_itip();
$emails = $this->get_user_emails();
// compose multipart message using PEAR:Mail_Mime
$method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
$message = $itip->compose_itip_message($event, $method);
// list existing attendees from $old event
$old_attendees = array();
foreach ((array)$old['attendees'] as $attendee) {
$old_attendees[] = $attendee['email'];
}
// send to every attendee
$sent = 0;
foreach ((array)$event['attendees'] as $attendee) {
// skip myself for obvious reasons
if (!$attendee['email'] || in_array($attendee['email'], $emails))
continue;
// which template to use for mail text
$is_new = !in_array($attendee['email'], $old_attendees);
$bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
$subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty'));
// finally send the message
if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message))
$sent++;
else
$sent = -100;
}
return $sent;
}
/**
* Compose a date string for the given event
*/
public function event_date_text($event, $tzinfo = false)
{
$fromto = '';
$duration = $event['end'] - $event['start'];
$this->date_format_defaults();
$date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
$time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']));
if ($event['allday']) {
$fromto = format_date($event['start'], $date_format);
if (($todate = format_date($event['end'], $date_format)) != $fromto)
$fromto .= ' - ' . $todate;
}
else if ($duration < 86400 && gmdate('d', $event['start']) == gmdate('d', $event['end'])) {
$fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
' - ' . format_date($event['end'], $time_format);
}
else {
$fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
' - ' . format_date($event['end'], $date_format) . ' ' . format_date($event['end'], $time_format);
}
// add timezone information
if ($tzinfo && ($tzname = $this->user_timezone->getName())) {
$fromto .= ' (' . $tzname . ')';
}
return $fromto;
}
/**
* Echo simple free/busy status text for the given user and time range
*/
public function freebusy_status()
{
$email = get_input_value('email', RCUBE_INPUT_GPC);
$start = get_input_value('start', RCUBE_INPUT_GPC);
$end = get_input_value('end', RCUBE_INPUT_GPC);
if (!$start) $start = time();
if (!$end) $end = $start + 3600;
$fbtypemap = array(calendar::FREEBUSY_UNKNOWN => 'UNKNOWN', calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE');
$status = 'UNKNOWN';
// if the backend has free-busy information
$fblist = $this->driver->get_freebusy_list($email, $start, $end);
if (is_array($fblist)) {
$status = 'FREE';
foreach ($fblist as $slot) {
list($from, $to, $type) = $slot;
if ($from < $end && $to > $start) {
$status = isset($type) && $fbtypemap[$type] ? $fbtypemap[$type] : 'BUSY';
break;
}
}
}
// let this information be cached for 5min
send_future_expire_header(300);
echo $status;
exit;
}
/**
* Return a list of free/busy time slots within the given period
* Echo data in JSON encoding
*/
public function freebusy_times()
{
$email = get_input_value('email', RCUBE_INPUT_GPC);
$start = get_input_value('start', RCUBE_INPUT_GPC);
$end = get_input_value('end', RCUBE_INPUT_GPC);
$interval = intval(get_input_value('interval', RCUBE_INPUT_GPC));
if (!$start) $start = time();
if (!$end) $end = $start + 86400 * 30;
if (!$interval) $interval = 60; // 1 hour
$fblist = $this->driver->get_freebusy_list($email, $start, $end);
$slots = array();
// build a list from $start till $end with blocks representing the fb-status
for ($s = 0, $t = $start; $t <= $end; $s++) {
$status = self::FREEBUSY_UNKNOWN;
$t_end = $t + $interval * 60;
// determine attendee's status
if (is_array($fblist)) {
$status = self::FREEBUSY_FREE;
foreach ($fblist as $slot) {
list($from, $to, $type) = $slot;
if ($from < $t_end && $to > $t) {
$status = isset($type) ? $type : self::FREEBUSY_BUSY;
if ($status == self::FREEBUSY_BUSY) // can't get any worse :-)
break;
}
}
}
$slots[$s] = $status;
$t = $t_end;
}
// let this information be cached for 5min
send_future_expire_header(300);
echo json_encode(array('email' => $email, 'start' => intval($start), 'end' => intval($t_end), 'interval' => $interval, 'slots' => $slots));
exit;
}
/**
* Handler for printing calendars
*/
public function print_view()
{
$title = $this->gettext('print');
$view = get_input_value('view', RCUBE_INPUT_GPC);
if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
$view = 'agendaDay';
$this->rc->output->set_env('view',$view);
if ($date = get_input_value('date', RCUBE_INPUT_GPC))
$this->rc->output->set_env('date', $date);
if ($range = get_input_value('range', RCUBE_INPUT_GPC))
$this->rc->output->set_env('listRange', intval($range));
if (isset($_REQUEST['sections']))
$this->rc->output->set_env('listSections', get_input_value('sections', RCUBE_INPUT_GPC));
if ($search = get_input_value('search', RCUBE_INPUT_GPC)) {
$this->rc->output->set_env('search', $search);
$title .= ' "' . $search . '"';
}
// Add CSS stylesheets to the page header
$skin = $this->rc->config->get('skin');
$this->include_stylesheet('skins/' . $skin . '/fullcalendar.css');
$this->include_stylesheet('skins/' . $skin . '/print.css');
// Add JS files to the page header
$this->include_script('print.js');
$this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css'));
$this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list'));
$this->rc->output->set_pagetitle($title);
$this->rc->output->send("calendar.print");
}
/**
*
*/
public function get_inline_ui()
{
foreach (array('save','cancel','savingdata') as $label)
$texts['calendar.'.$label] = $this->gettext($label);
$texts['calendar.new_event'] = $this->gettext('createfrommail');
$this->ui->init_templates();
$this->ui->calendar_list(); # set env['calendars']
echo $this->api->output->parse('calendar.eventedit', false, false);
echo html::tag('script', array('type' => 'text/javascript'),
"rcmail.set_env('calendars', " . json_encode($this->api->output->env['calendars']) . ");\n".
"rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n".
"rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n".
"rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n".
"rcmail.add_label(" . json_encode($texts) . ");\n"
);
exit;
}
/**
* Compare two event objects and return differing properties
*
* @param array Event A
* @param array Event B
* @return array List of differing event properties
*/
public static function event_diff($a, $b)
{
$diff = array();
$ignore = array('changed' => 1, 'attachments' => 1);
foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
if (!$ignore[$key] && $a[$key] != $b[$key])
$diff[] = $key;
}
// only compare number of attachments
if (count($a['attachments']) != count($b['attachments']))
$diff[] = 'attachments';
return $diff;
}
/**** Event invitation plugin hooks ****/
/**
* Handler for URLs that allow an invitee to respond on his invitation mail
*/
public function itip_attend_response($p)
{
if ($p['action'] == 'attend') {
$this->rc->output->set_env('task', 'calendar'); // override some env vars
$this->rc->output->set_env('keep_alive', 0);
$this->rc->output->set_pagetitle($this->gettext('calendar'));
$itip = $this->load_itip();
$token = get_input_value('_t', RCUBE_INPUT_GPC);
// read event info stored under the given token
if ($invitation = $itip->get_invitation($token)) {
$this->token = $token;
$this->event = $invitation['event'];
// show message about cancellation
if ($invitation['cancelled']) {
$this->invitestatus = html::div('rsvp-status declined', $this->gettext('eventcancelled'));
}
// save submitted RSVP status
else if (!empty($_POST['rsvp'])) {
$status = null;
foreach (array('accepted','tentative','declined') as $method) {
if ($_POST['rsvp'] == $this->gettext('itip' . $method)) {
$status = $method;
break;
}
}
// send itip reply to organizer
if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) {
$this->invitestatus = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
}
else
$this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1);
}
$this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform'));
$this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox'));
if (!$this->invitestatus)
$this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons'));
$this->rc->output->set_pagetitle($this->gettext('itipinvitation') . ' ' . $this->event['title']);
}
else
$this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1);
$this->rc->output->send('calendar.itipattend');
}
}
/**
*
*/
public function itip_event_inviteform($p)
{
$hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token));
return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true)) . $hidden->show();
}
/**
* Check mail message structure of there are .ics files attached
*/
public function mail_message_load($p)
{
$this->message = $p['object'];
$itip_part = null;
// check all message parts for .ics files
foreach ((array)$this->message->mime_parts as $idx => $part) {
if ($this->is_vcalendar($part)) {
if ($part->ctype_parameters['method'])
$itip_part = $part->mime_id;
else
$this->ics_parts[] = $part->mime_id;
}
}
// priorize part with method parameter
if ($itip_part)
$this->ics_parts = array($itip_part);
}
/**
* Add UI element to copy event invitations or updates to the calendar
*/
public function mail_messagebody_html($p)
{
// load iCalendar functions (if necessary)
if (!empty($this->ics_parts)) {
$this->get_ical();
}
$html = '';
foreach ($this->ics_parts as $mime_id) {
$part = $this->message->mime_parts[$mime_id];
$charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET;
$events = $this->ical->import($this->message->get_part_content($mime_id), $charset);
$title = $this->gettext('title');
// successfully parsed events?
if (empty($events))
continue;
// show a box for every event in the file
foreach ($events as $idx => $event) {
// define buttons according to method
if ($this->ical->method == 'REPLY') {
$title = $this->gettext('itipreply');
$buttons = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('updateattendeestatus'),
));
}
else if ($this->ical->method == 'REQUEST') {
$emails = $this->get_user_emails();
$title = $event['SEQUENCE'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation');
// add (hidden) buttons and activate them from asyncronous request
foreach (array('accepted','tentative','declined') as $method) {
$rsvp_buttons .= html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "', '$method')",
'value' => $this->gettext('itip' . $method),
));
}
$import_button = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('importtocalendar'),
));
// check my status
$status = 'unknown';
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array($attendee['email'], $emails)) {
$status = strtoupper($attendee['status']);
break;
}
}
$dom_id = asciiwords($event['uid'], true);
$buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $rsvp_buttons);
$buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
$buttons_pre = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
$this->rc->output->add_script('rcube_calendar.fetch_event_rsvp_status(' . json_serialize(array('uid' => $event['uid'], 'changed' => $event['changed'], 'fallback' => $status)) . ')', 'docready');
}
else if ($this->ical->method == 'CANCEL') {
$title = $this->gettext('itipcancellation');
// create buttons to be activated from async request checking existence of this event in local calendars
$button_import = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('importtocalendar'),
));
$button_remove = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.remove_event_from_mail('" . JQ($event['uid']) . "', '" . JQ($event['title']) . "')",
'value' => $this->gettext('removefromcalendar'),
));
$dom_id = asciiwords($event['uid'], true);
$buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove);
$buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $button_import);
$buttons_pre = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
$this->rc->output->add_script('rcube_calendar.fetch_event_rsvp_status(' . json_serialize(array('uid' => $event['uid'], 'changed' => $event['changed'], 'fallback' => 'CANCELLED')) . ')', 'docready');
}
else {
$buttons = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('importtocalendar'),
));
}
// show event details with buttons
$html .= html::div('calendar-invitebox', $this->ui->event_details_table($event, $title) . $buttons_pre . html::div('rsvp-buttons', $buttons));
// limit listing
if ($idx >= 3)
break;
}
}
// prepend event boxes to message body
if ($html) {
$this->ui->init();
$p['content'] = $html . $p['content'];
$this->rc->output->add_label('calendar.savingdata','calendar.deleteventconfirm');
}
return $p;
}
/**
* Handler for POST request to import an event attached to a mail message
*/
public function mail_import_event()
{
$uid = get_input_value('_uid', RCUBE_INPUT_POST);
$mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
$mime_id = get_input_value('_part', RCUBE_INPUT_POST);
$status = get_input_value('_status', RCUBE_INPUT_POST);
$charset = RCMAIL_CHARSET;
// establish imap connection
$this->rc->imap_connect();
$this->rc->imap->set_mailbox($mbox);
if ($uid && $mime_id) {
list($mime_id, $index) = explode(':', $mime_id);
$part = $this->rc->imap->get_message_part($uid, $mime_id);
if ($part->ctype_parameters['charset'])
$charset = $part->ctype_parameters['charset'];
$headers = $this->rc->imap->get_headers($uid);
}
$events = $this->get_ical()->import($part, $charset);
$error_msg = $this->gettext('errorimportingevent');
$success = false;
// successfully parsed events?
if (!empty($events) && ($event = $events[$index])) {
// find writeable calendar to store event
$cal_id = !empty($_REQUEST['_calendar']) ? get_input_value('_calendar', RCUBE_INPUT_POST) : $this->rc->config->get('calendar_default_calendar');
$calendars = $this->driver->list_calendars();
$calendar = $calendars[$cal_id] ? $calendars[$cal_id] : null;
if (!$calendar || $calendar['readonly']) {
foreach ($calendars as $cal) {
if (!$cal['readonly']) {
$calendar = $cal;
break;
}
}
}
// update my attendee status according to submitted method
if (!empty($status)) {
$organizer = null;
$emails = $this->get_user_emails();
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
}
else if ($attendee['email'] && in_array($attendee['email'], $emails)) {
$event['attendees'][$i]['status'] = strtoupper($status);
}
}
}
// save to calendar
if ($calendar && !$calendar['readonly']) {
$event['id'] = $event['uid'];
$event['calendar'] = $calendar['id'];
// check for existing event with the same UID
$existing = $this->driver->get_event($event['uid'], true);
if ($existing) {
// only update attendee status
if ($this->ical->method == 'REPLY') {
// try to identify the attendee using the email sender address
$sender = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from, $m) ? $m[1] : '';
$sender_utf = rcube_idn_to_utf8($sender);
$existing_attendee = -1;
foreach ($existing['attendees'] as $i => $attendee) {
if ($sender && ($attendee['email'] == $sender || $attendee['email'] == $sender_utf)) {
$existing_attendee = $i;
break;
}
}
$event_attendee = null;
foreach ($event['attendees'] as $attendee) {
if ($sender && ($attendee['email'] == $sender || $attendee['email'] == $sender_utf)) {
$event_attendee = $attendee;
break;
}
}
// found matching attendee entry in both existing and new events
if ($existing_attendee >= 0 && $event_attendee) {
$existing['attendees'][$existing_attendee] = $event_attendee;
$success = $this->driver->edit_event($existing);
}
// update the entire attendees block
else if ($event['changed'] >= $existing['changed'] && $event['attendees']) {
$existing['attendees'] = $event['attendees'];
$success = $this->driver->edit_event($existing);
}
else {
$error_msg = $this->gettext('newerversionexists');
}
}
// import the (newer) event
// TODO: compare SEQUENCE numbers instead of changed dates
else if ($event['changed'] >= $existing['changed']) {
$success = $this->driver->edit_event($event);
}
else if (!empty($status)) {
$existing['attendees'] = $event['attendees'];
$success = $this->driver->edit_event($existing);
}
else
$error_msg = $this->gettext('newerversionexists');
}
else if (!$existing && $status != 'declined') {
$success = $this->driver->new_event($event);
}
else if ($status == 'declined')
$error_msg = null;
}
else if ($status == 'declined')
$error_msg = null;
else
$error_msg = $this->gettext('nowritecalendarfound');
}
if ($success) {
$message = $this->ical->method == 'REPLY' ? 'attendeupdateesuccess' : 'importedsuccessfully';
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
$error_msg = null;
}
else if ($error_msg)
$this->rc->output->command('display_message', $error_msg, 'error');
// send iTip reply
if ($this->ical->method == 'REQUEST' && $organizer && !in_array($organizer['email'], $emails) && !$error_msg) {
$itip = $this->load_itip();
if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
$this->rc->output->send();
}
/**
* Read email message and return contents for a new event based on that message
*/
public function mail_message2event()
{
$uid = get_input_value('_uid', RCUBE_INPUT_POST);
$mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
$event = array();
// establish imap connection
$this->rc->imap_connect();
$this->rc->imap->set_mailbox($mbox);
$message = new rcube_message($uid);
if ($message->headers) {
$event['title'] = trim($message->subject);
$event['description'] = trim($message->first_text_part());
// copy mail attachments to event
if ($message->attachments) {
$eventid = 'cal:';
if (!is_array($_SESSION['event_session']) || $_SESSION['event_session']['id'] != $eventid) {
$_SESSION['event_session'] = array();
$_SESSION['event_session']['id'] = $eventid;
$_SESSION['event_session']['attachments'] = array();
}
foreach ((array)$message->attachments as $part) {
$attachment = array(
'data' => $this->rc->imap->get_message_part($uid, $part->mime_id, $part),
'size' => $part->size,
'name' => $part->filename,
'mimetype' => $part->mimetype,
'group' => $eventid,
);
$attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
// store new attachment in session
unset($attachment['status'], $attachment['abort'], $attachment['data']);
$_SESSION['event_session']['attachments'][$id] = $attachment;
$attachment['id'] = 'rcmfile' . $attachment['id']; # add prefix to consider it 'new'
$event['attachments'][] = $attachment;
}
}
}
$this->rc->output->command('plugin.mail2event_dialog', $event);
}
else {
$this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
}
$this->rc->output->send();
}
/**
* Checks if specified message part is a vcalendar data
*
* @param rcube_message_part Part object
* @return boolean True if part is of type vcard
*/
private function is_vcalendar($part)
{
return (
in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
// Apple sends files as application/x-any (!?)
($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename))
);
}
/**
* Get a list of email addresses of the current user (from login and identities)
*/
private function get_user_emails()
{
$emails = array($this->rc->user->get_username());
foreach ($this->rc->user->list_identities() as $identity)
$emails[] = $identity['email'];
return array_unique($emails);
}
/**
* Build an absolute URL with the given parameters
*/
public function get_url($param = array())
{
$param += array('task' => 'calendar');
$schema = 'http';
$default_port = 80;
if (rcube_https_check()) {
$schema = 'https';
$default_port = 143;
}
$url = $schema . '://' . $_SERVER['HTTP_HOST'];
if ($_SERVER['SERVER_PORT'] != $default_port)
$url .= ':' . $_SERVER['SERVER_PORT'];
if (dirname($_SERVER['SCRIPT_NAME']) != '/')
$url .= dirname($_SERVER['SCRIPT_NAME']);
$url .= preg_replace('!^\./!', '/', $this->rc->url($param));
return $url;
}
}
-
diff --git a/plugins/calendar/calendar_base.js b/plugins/calendar/calendar_base.js
index 91c2cc04..63e2147e 100644
--- a/plugins/calendar/calendar_base.js
+++ b/plugins/calendar/calendar_base.js
@@ -1,296 +1,303 @@
/**
* Base Javascript class for the Calendar plugin
*
* @version 0.7-beta
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <roundcube@gmail.com>
*
- * Copyright (C) 2010, Lazlo Westerhof - Netherlands
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
// Basic setup for Roundcube calendar client class
function rcube_calendar(settings)
{
// member vars
this.ui;
this.ui_loaded = false;
this.settings = settings;
this.alarm_ids = [];
this.alarm_dialog = null;
this.snooze_popup = null;
this.dismiss_link = null;
// private vars
var me = this;
// quote html entities
var Q = this.quote_html = function(str)
{
return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
};
// create a nice human-readable string for the date/time range
this.event_date_text = function(event)
{
var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000;
if (event.allDay)
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + (duration > 86400 || event.start.getDay() != event.end.getDay() ? ' &mdash; ' + $.fullCalendar.formatDate(event.end, settings['date_format']) : '');
else if (duration < 86400 && event.start.getDay() == event.end.getDay())
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format']) + ' &mdash; '
+ $.fullCalendar.formatDate(event.end, settings['time_format']);
else
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format']) + ' &mdash; '
+ $.fullCalendar.formatDate(event.end, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.end, settings['time_format']);
return fromto;
};
// display a notification for the given pending alarms
this.display_alarms = function(alarms) {
// clear old alert first
if (this.alarm_dialog)
this.alarm_dialog.dialog('destroy');
this.alarm_dialog = $('<div>').attr('id', 'alarm-display');
var actions, adismiss, asnooze, alarm, html, event_ids = [];
for (var actions, html, alarm, i=0; i < alarms.length; i++) {
alarm = alarms[i];
alarm.start = $.fullCalendar.parseISO8601(alarm.start, true);
alarm.end = $.fullCalendar.parseISO8601(alarm.end, true);
event_ids.push(alarm.id);
html = '<h3 class="event-title">' + Q(alarm.title) + '</h3>';
html += '<div class="event-section">' + Q(alarm.location || '') + '</div>';
html += '<div class="event-section">' + Q(this.event_date_text(alarm)) + '</div>';
adismiss = $('<a href="#" class="alarm-action-dismiss"></a>').html(rcmail.gettext('dismiss','calendar')).click(function(){
me.dismiss_link = $(this);
me.dismiss_alarm(me.dismiss_link.data('id'), 0);
});
asnooze = $('<a href="#" class="alarm-action-snooze"></a>').html(rcmail.gettext('snooze','calendar')).click(function(){
me.snooze_dropdown($(this));
});
actions = $('<div>').addClass('alarm-actions').append(adismiss.data('id', alarm.id)).append(asnooze.data('id', alarm.id));
$('<div>').addClass('alarm-item').html(html).append(actions).appendTo(this.alarm_dialog);
}
var buttons = {};
buttons[rcmail.gettext('dismissall','calendar')] = function() {
// submit dismissed event_ids to server
me.dismiss_alarm(me.alarm_ids.join(','), 0);
$(this).dialog('close');
};
this.alarm_dialog.appendTo(document.body).dialog({
modal: false,
resizable: true,
closeOnEscape: false,
dialogClass: 'alarm',
title: '<span class="ui-icon ui-icon-alert" style="float:left; margin:0 4px 0 0"></span>' + rcmail.gettext('alarmtitle', 'calendar'),
buttons: buttons,
close: function() {
$('#alarm-snooze-dropdown').hide();
$(this).dialog('destroy').remove();
me.alarm_dialog = null;
me.alarm_ids = null;
},
drag: function(event, ui) {
$('#alarm-snooze-dropdown').hide();
}
});
this.alarm_ids = event_ids;
};
// show a drop-down menu with a selection of snooze times
this.snooze_dropdown = function(link)
{
if (!this.snooze_popup) {
this.snooze_popup = $('#alarm-snooze-dropdown');
// create popup if not found
if (!this.snooze_popup.length) {
this.snooze_popup = $('<div>').attr('id', 'alarm-snooze-dropdown').addClass('popupmenu').appendTo(document.body);
this.snooze_popup.html(rcmail.env.snooze_select)
}
$('#alarm-snooze-dropdown a').click(function(e){
var time = String(this.href).replace(/.+#/, '');
me.dismiss_alarm($('#alarm-snooze-dropdown').data('id'), time);
return false;
});
}
// hide visible popup
if (this.snooze_popup.is(':visible') && this.snooze_popup.data('id') == link.data('id')) {
this.snooze_popup.hide();
this.dismiss_link = null;
}
else { // open popup below the clicked link
var pos = link.offset();
pos.top += link.height() + 2;
this.snooze_popup.data('id', link.data('id')).css({ top:Math.floor(pos.top)+'px', left:Math.floor(pos.left)+'px' }).show();
this.dismiss_link = link;
}
};
// dismiss or snooze alarms for the given event
this.dismiss_alarm = function(id, snooze)
{
$('#alarm-snooze-dropdown').hide();
rcmail.http_post('calendar/event', { action:'dismiss', e:{ id:id, snooze:snooze } });
// remove dismissed alarm from list
if (this.dismiss_link) {
this.dismiss_link.closest('div.alarm-item').hide();
var new_ids = jQuery.grep(this.alarm_ids, function(v){ return v != id; });
if (new_ids.length)
this.alarm_ids = new_ids;
else
this.alarm_dialog.dialog('close');
}
this.dismiss_link = null;
};
// create new event from current mail message
this.create_from_mail = function()
{
var uid;
if ((uid = rcmail.get_single_uid())) {
// load calendar UI (scripts and edit dialog template)
if (!this.ui_loaded) {
$.when(
$.getScript('./plugins/calendar/calendar_ui.js'),
$.get(rcmail.url('calendar/inlineui'), function(html){ $(document.body).append(html); }, 'html')
).then(function() {
// register attachments form
rcmail.gui_object('attachmentlist', 'attachmentlist');
// disable attendees feature (autocompletion and stuff is not initialized)
for (var c in rcmail.env.calendars)
rcmail.env.calendars[c].attendees = false;
me.ui_loaded = true;
me.ui = new rcube_calendar_ui(me.settings);
me.create_from_mail(); // start over
});
return;
}
else {
// get message contents for event dialog
var lock = rcmail.set_busy(true, 'loading');
rcmail.http_post('calendar/mailtoevent', {
'_mbox': rcmail.env.mailbox,
'_uid': uid
}, lock);
}
}
};
// callback function triggered from server with contents for the new event
this.mail2event_dialog = function(event)
{
if (event.title) {
this.ui.add_event(event);
rcmail.message_list.blur();
}
};
}
// static methods
rcube_calendar.add_event_from_mail = function(mime_id, status)
{
var lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar/mailimportevent', {
'_uid': rcmail.env.uid,
'_mbox': rcmail.env.mailbox,
'_part': mime_id,
'_calendar': $('#calendar-saveto').val(),
'_status': status
}, lock);
return false;
};
rcube_calendar.remove_event_from_mail = function(uid, title)
{
if (confirm(rcmail.gettext('calendar.deleteventconfirm'))) {
var lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar/event', {
e:{ uid:uid },
action: 'remove'
}, lock);
}
};
rcube_calendar.fetch_event_rsvp_status = function(event)
{
/*
var id = event.uid.replace(rcmail.identifier_expr, '');
$('#import-'+id+', #rsvp-'+id+', div.rsvp-status').hide();
$('#loading-'+id).show();
*/
rcmail.http_post('calendar/event', {
e:event,
action:'rsvp-status'
});
};
// extend jQuery
(function($){
$.fn.serializeJSON = function(){
var json = {};
jQuery.map($(this).serializeArray(), function(n, i) {
json[n['name']] = n['value'];
});
return json;
};
})(jQuery);
/* calendar plugin initialization (for non-calendar tasks) */
window.rcmail && rcmail.addEventListener('init', function(evt) {
if (rcmail.task != 'calendar') {
var cal = new rcube_calendar(rcmail.env.calendar_settings);
rcmail.addEventListener('plugin.display_alarms', function(alarms){ cal.display_alarms(alarms); });
rcmail.addEventListener('plugin.update_event_rsvp_status', function(p){
if (p.html)
$('#loading-'+p.id).hide().after(p.html);
else
$('#loading-'+p.id).hide();
$('#'+p.action+'-'+p.id).show().append(p.select);
});
// register create-from-mail command to message_commands array
if (rcmail.env.task == 'mail') {
// place link above 'view source'
$('#messagemenu a.calendarlink').parent().insertBefore($('#messagemenu a.sourcelink').parent());
rcmail.register_command('calendar-create-from-mail', function() { cal.create_from_mail() });
rcmail.addEventListener('plugin.mail2event_dialog', function(p){ cal.mail2event_dialog(p) });
rcmail.addEventListener('plugin.unlock_saving', function(p){ rcmail.set_busy(false, null, cal.ui.saving_lock); });
if (rcmail.env.action != 'show') {
rcmail.env.message_commands.push('calendar-create-from-mail');
rcmail.add_element($('<a>'));
}
else
rcmail.enable_command('calendar-create-from-mail', true);
}
}
rcmail.addEventListener('plugin.ping_url', function(p){
var action = p.action;
p.action = p.event = null;
new Image().src = rcmail.url(action, p);
});
});
-
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index b5982ed9..170b75e8 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -1,2746 +1,2754 @@
/**
* Client UI Javascript for the Calendar plugin
*
* @version 0.7-beta
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <roundcube@gmail.com>
*
- * Copyright (C) 2010, Lazlo Westerhof - Netherlands
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
// Roundcube calendar UI client class
function rcube_calendar_ui(settings)
{
// extend base class
rcube_calendar.call(this, settings);
/*** member vars ***/
this.is_loading = false;
this.selected_event = null;
this.selected_calendar = null;
this.search_request = null;
this.saving_lock = null;
/*** private vars ***/
var DAY_MS = 86400000;
var HOUR_MS = 3600000;
var me = this;
var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0);
var client_timezone = new Date().getTimezoneOffset();
var day_clicked = day_clicked_ts = 0;
var ignore_click = false;
var event_defaults = { free_busy:'busy' };
var event_attendees = [];
var attendees_list;
var freebusy_ui = { workinhoursonly:false, needsupdate:false };
var freebusy_data = {};
var event_resizing = false;
var current_view = null;
var exec_deferred = bw.ie6 ? 5 : 1;
var sensitivitylabels = { 0:rcmail.gettext('public','calendar'), 1:rcmail.gettext('private','calendar'), 2:rcmail.gettext('confidential','calendar') };
var ui_loading = rcmail.set_busy(true, 'loading');
// general datepicker settings
var datepicker_settings = {
// translate from fullcalendar format to datepicker format
dateFormat: settings['date_format'].replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'),
firstDay : settings['first_day'],
dayNamesMin: settings['days_short'],
monthNames: settings['months'],
monthNamesShort: settings['months'],
changeMonth: false,
showOtherMonths: true,
selectOtherMonths: true
};
/*** private methods ***/
var Q = this.quote_html;
var text2html = function(str, maxlen, maxlines)
{
var html = Q(String(str));
// limit visible text length
if (maxlen) {
var morelink = ' <a href="#more" onclick="$(this).hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','calendar')+'</a><span style="display:none">',
lines = html.split(/\r?\n/),
words, out = '', len = 0;
for (var i=0; i < lines.length; i++) {
len += lines[i].length;
if (maxlines && i == maxlines - 1) {
out += lines[i] + '\n' + morelink;
maxlen = html.length * 2;
}
else if (len > maxlen) {
len = out.length;
words = lines[i].split(' ');
for (var j=0; j < words.length; j++) {
len += words[j].length + 1;
out += words[j] + ' ';
if (len > maxlen) {
out += morelink;
maxlen = html.length * 2;
}
}
out += '\n';
}
else
out += lines[i] + '\n';
}
if (maxlen > str.length)
out += '</span>';
html = out;
}
// simple link parser (similar to rcube_string_replacer class in PHP)
var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})';
var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-';
var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)?', 'ig');
var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig');
return html
.replace(link_pattern, '<a href="$1$2" target="_blank">$1$2</a>')
.replace(mailto_pattern, '<a href="mailto:$1">$1</a>')
.replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"')
.replace(/\n/g, "<br/>");
};
// same as str.split(delimiter) but it ignores delimiters within quoted strings
var explode_quoted_string = function(str, delimiter)
{
var result = [],
strlen = str.length,
q, p, i, char, last;
for (q = p = i = 0; i < strlen; i++) {
char = str.charAt(i);
if (char == '"' && last != '\\') {
q = !q;
}
else if (!q && char == delimiter) {
result.push(str.substring(p, i));
p = i + 1;
}
last = char;
}
result.push(str.substr(p));
return result;
};
// from time and date strings to a real date object
var parse_datetime = function(time, date)
{
// we use the utility function from datepicker to parse dates
var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date();
var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/);
if (!isNaN(time_arr[0])) {
date.setHours(time_arr[0]);
if (time.match(/p[.m]*/i) && date.getHours() < 12)
date.setHours(parseInt(time_arr[0]) + 12);
else if (time.match(/a[.m]*/i) && date.getHours() == 12)
date.setHours(0);
}
if (!isNaN(time_arr[1]))
date.setMinutes(time_arr[1]);
return date;
};
// clone the given date object and optionally adjust time
var clone_date = function(date, adjust)
{
var d = new Date(date.getTime());
// set time to 00:00
if (adjust == 1) {
d.setHours(0);
d.setMinutes(0);
}
// set time to 23:59
else if (adjust == 2) {
d.setHours(23);
d.setMinutes(59);
}
return d;
};
// convert the given Date object into a unix timestamp respecting browser's and user's timezone settings
var date2unixtime = function(date)
{
var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; // adjust DST offset
return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset);
};
var fromunixtime = function(ts)
{
ts -= gmt_offset * 3600;
var date = new Date(ts * 1000),
dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;
if (dst_offset) // adjust DST offset
date.setTime((ts + 3600) * 1000);
return date;
};
// determine whether the given date is on a weekend
var is_weekend = function(date)
{
return date.getDay() == 0 || date.getDay() == 6;
};
var is_workinghour = function(date)
{
if (settings['work_start'] > settings['work_end'])
return date.getHours() >= settings['work_start'] || date.getHours() < settings['work_end'];
else
return date.getHours() >= settings['work_start'] && date.getHours() < settings['work_end'];
};
// check if the event has 'real' attendees, excluding the current user
var has_attendees = function(event)
{
return (event.attendees && (event.attendees.length > 1 || event.attendees[0].email != settings.identity.email));
};
// check if the current user is an attendee of this event
var is_attendee = function(event, role)
{
for (var i=0; event.attendees && i < event.attendees.length; i++) {
if ((!role || event.attendees[i].role == role) && event.attendees[i].email && settings.identity.emails.indexOf(';'+event.attendees[i].email) >= 0)
return true;
}
return false;
};
// check if the current user is the organizer
var is_organizer = function(event)
{
return is_attendee(event, 'ORGANIZER') || !event.id;
};
// create a nice human-readable string for the date/time range
var event_date_text = function(event)
{
var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000;
if (event.allDay)
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + (duration > 86400 || event.start.getDay() != event.end.getDay() ? ' &mdash; ' + $.fullCalendar.formatDate(event.end, settings['date_format']) : '');
else if (duration < 86400 && event.start.getDay() == event.end.getDay())
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format']) + ' &mdash; '
+ $.fullCalendar.formatDate(event.end, settings['time_format']);
else
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format']) + ' &mdash; '
+ $.fullCalendar.formatDate(event.end, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.end, settings['time_format']);
return fromto;
};
var load_attachment = function(event, att)
{
var qstring = '_id='+urlencode(att.id)+'&_event='+urlencode(event.recurrence_id||event.id)+'&_cal='+urlencode(event.calendar);
// open attachment in frame if it's of a supported mimetype
if (id && att.mimetype && $.inArray(att.mimetype, rcmail.mimetypes)>=0) {
rcmail.attachment_win = window.open(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', 'rcubeeventattachment');
if (rcmail.attachment_win) {
window.setTimeout(function() { rcmail.attachment_win.focus(); }, 10);
return;
}
}
rcmail.goto_url('get-attachment', qstring+'&_download=1', false);
};
// build event attachments list
var event_show_attachments = function(list, container, event, edit)
{
var i, id, len, img, content, li, elem,
ul = document.createElement('UL');
for (i=0, len=list.length; i<len; i++) {
li = document.createElement('LI');
elem = list[i];
if (edit) {
rcmail.env.attachments[elem.id] = elem;
// delete icon
content = document.createElement('A');
content.href = '#delete';
content.title = rcmail.gettext('delete');
$(content).click({id: elem.id}, function(e) { remove_attachment(this, e.data.id); return false; });
if (!rcmail.env.deleteicon)
content.innerHTML = rcmail.gettext('delete');
else {
img = document.createElement('IMG');
img.src = rcmail.env.deleteicon;
img.alt = rcmail.gettext('delete');
content.appendChild(img);
}
li.appendChild(content);
}
// name/link
content = document.createElement('A');
content.innerHTML = list[i].name;
content.href = '#load';
$(content).click({event: event, att: elem}, function(e) {
load_attachment(e.data.event, e.data.att); return false; });
li.appendChild(content);
ul.appendChild(li);
}
if (edit && rcmail.gui_objects.attachmentlist) {
ul.id = rcmail.gui_objects.attachmentlist.id;
rcmail.gui_objects.attachmentlist = ul;
}
container.empty().append(ul);
};
var remove_attachment = function(elem, id)
{
$(elem.parentNode).hide();
rcmail.env.deleted_attachments.push(id);
delete rcmail.env.attachments[id];
};
// event details dialog (show only)
var event_show_dialog = function(event)
{
var $dialog = $("#eventshow").dialog('close').removeClass().addClass('uidialog');
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
me.selected_event = event;
$dialog.find('div.event-section, div.event-line').hide();
$('#event-title').html(Q(event.title)).show();
if (event.location)
$('#event-location').html('@ ' + text2html(event.location)).show();
if (event.description)
$('#event-description').show().children('.event-text').html(text2html(event.description, 300, 6));
// render from-to in a nice human-readable way
$('#event-date').html(Q(me.event_date_text(event))).show();
if (event.recurrence && event.recurrence_text)
$('#event-repeat').show().children('.event-text').html(Q(event.recurrence_text));
if (event.alarms && event.alarms_text)
$('#event-alarm').show().children('.event-text').html(Q(event.alarms_text));
if (calendar.name)
$('#event-calendar').show().children('.event-text').html(Q(calendar.name)).removeClass().addClass('event-text').addClass('cal-'+calendar.id);
if (event.categories)
$('#event-category').show().children('.event-text').html(Q(event.categories)).removeClass().addClass('event-text cat-'+String(event.categories).replace(rcmail.identifier_expr, ''));
if (event.free_busy)
$('#event-free-busy').show().children('.event-text').html(Q(rcmail.gettext(event.free_busy, 'calendar')));
if (event.priority > 0) {
var priolabels = [ '', rcmail.gettext('high'), rcmail.gettext('highest'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
$('#event-priority').show().children('.event-text').html(Q(event.priority+' '+priolabels[event.priority]));
}
if (event.sensitivity != 0) {
var sensitivityclasses = { 0:'public', 1:'private', 2:'confidential' };
$('#event-sensitivity').show().children('.event-text').html(Q(sensitivitylabels[event.sensitivity]));
$dialog.addClass('sensitivity-'+sensitivityclasses[event.sensitivity]);
}
// create attachments list
if ($.isArray(event.attachments)) {
event_show_attachments(event.attachments, $('#event-attachments').children('.event-text'), event);
if (event.attachments.length > 0) {
$('#event-attachments').show();
}
}
else if (calendar.attachments) {
// fetch attachments, some drivers doesn't set 'attachments' prop of the event?
}
// list event attendees
if (calendar.attendees && event.attendees) {
var data, dispname, organizer = false, rsvp = false, html = '';
for (var j=0; j < event.attendees.length; j++) {
data = event.attendees[j];
dispname = Q(data.name || data.email);
if (data.email) {
dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
if (data.role == 'ORGANIZER')
organizer = true;
else if ((data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE') && settings.identity.emails.indexOf(';'+data.email) >= 0)
rsvp = data.status.toLowerCase();
}
html += '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '">' + dispname + '</span> ';
// stop listing attendees
if (j == 7 && event.attendees.length >= 7) {
html += ' <em>' + rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1) + '</em>';
break;
}
}
if (html && (event.attendees.length > 1 || !organizer)) {
$('#event-attendees').show()
.children('.event-text')
.html(html)
.find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
}
$('#event-rsvp')[(rsvp?'show':'hide')]();
$('#event-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+rsvp+']').prop('disabled', true);
}
var buttons = {};
if (calendar.editable && event.editable !== false) {
buttons[rcmail.gettext('edit', 'calendar')] = function() {
event_edit_dialog('edit', event);
};
buttons[rcmail.gettext('remove', 'calendar')] = function() {
me.delete_event(event);
$dialog.dialog('close');
};
}
else {
buttons[rcmail.gettext('close', 'calendar')] = function(){
$dialog.dialog('close');
};
}
// open jquery UI dialog
$dialog.dialog({
modal: false,
resizable: !bw.ie6,
closeOnEscape: (!bw.ie6 && !bw.ie7), // disable for performance reasons
title: null,
close: function() {
$dialog.dialog('destroy').hide();
},
buttons: buttons,
minWidth: 320,
width: 420
}).show();
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 420);
/*
// add link for "more options" drop-down
$('<a>')
.attr('href', '#')
.html('More Options')
.addClass('dropdown-link')
.click(function(){ return false; })
.insertBefore($dialog.parent().find('.ui-dialog-buttonset').children().first());
*/
};
// bring up the event dialog (jquery-ui popup)
var event_edit_dialog = function(action, event)
{
// close show dialog first
$("#eventshow").dialog('close');
var $dialog = $('<div>');
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:action=='new' };
me.selected_event = $.extend($.extend({}, event_defaults), event); // clone event object (with defaults)
event = me.selected_event; // change reference to clone
freebusy_ui.needsupdate = false;
// reset dialog first
$('#eventtabs').get(0).reset();
// event details
var title = $('#edit-title').val(event.title || '');
var location = $('#edit-location').val(event.location || '');
var description = $('#edit-description').html(event.description || '');
var categories = $('#edit-categories').val(event.categories);
var calendars = $('#edit-calendar').val(event.calendar);
var freebusy = $('#edit-free-busy').val(event.free_busy);
var priority = $('#edit-priority').val(event.priority);
var sensitivity = $('#edit-sensitivity').val(event.sensitivity);
var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
var startdate = $('#edit-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
var starttime = $('#edit-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
var enddate = $('#edit-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
var endtime = $('#edit-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
var allday = $('#edit-allday').get(0);
var notify = $('#edit-attendees-donotify').get(0);
var invite = $('#edit-attendees-invite').get(0);
notify.checked = has_attendees(event), invite.checked = true;
if (event.allDay) {
starttime.val("12:00").hide();
endtime.val("13:00").hide();
allday.checked = true;
}
else {
allday.checked = false;
}
// set alarm(s)
// TODO: support multiple alarm entries
if (event.alarms) {
if (typeof event.alarms == 'string')
event.alarms = event.alarms.split(';');
for (var alarm, i=0; i < event.alarms.length; i++) {
alarm = String(event.alarms[i]).split(':');
if (!alarm[1] && alarm[0]) alarm[1] = 'DISPLAY';
$('select.edit-alarm-type').val(alarm[1]);
if (alarm[0].match(/@(\d+)/)) {
var ondate = fromunixtime(parseInt(RegExp.$1));
$('select.edit-alarm-offset').val('@');
$('input.edit-alarm-date').val($.fullCalendar.formatDate(ondate, settings['date_format']));
$('input.edit-alarm-time').val($.fullCalendar.formatDate(ondate, settings['time_format']));
}
else if (alarm[0].match(/([-+])(\d+)([MHD])/)) {
$('input.edit-alarm-value').val(RegExp.$2);
$('select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3);
}
}
}
// set correct visibility by triggering onchange handlers
$('select.edit-alarm-type, select.edit-alarm-offset').change();
// enable/disable alarm property according to backend support
$('#edit-alarms')[(calendar.alarms ? 'show' : 'hide')]();
// check categories drop-down: add value if not exists
if (event.categories && !categories.find("option[value='"+event.categories+"']").length) {
$('<option>').attr('value', event.categories).text(event.categories).appendTo(categories).prop('selected', true);
}
// set recurrence form
var recurrence, interval, rrtimes, rrenddate;
var load_recurrence_tab = function()
{
recurrence = $('#edit-recurrence-frequency').val(event.recurrence ? event.recurrence.FREQ : '').change();
interval = $('select.edit-recurrence-interval').val(event.recurrence ? event.recurrence.INTERVAL : 1);
rrtimes = $('#edit-recurrence-repeat-times').val(event.recurrence ? event.recurrence.COUNT : 1);
rrenddate = $('#edit-recurrence-enddate').val(event.recurrence && event.recurrence.UNTIL ? $.fullCalendar.formatDate(new Date(event.recurrence.UNTIL*1000), settings['date_format']) : '');
$('input.edit-recurrence-until:checked').prop('checked', false);
var weekdays = ['SU','MO','TU','WE','TH','FR','SA'];
var rrepeat_id = '#edit-recurrence-repeat-forever';
if (event.recurrence && event.recurrence.COUNT) rrepeat_id = '#edit-recurrence-repeat-count';
else if (event.recurrence && event.recurrence.UNTIL) rrepeat_id = '#edit-recurrence-repeat-until';
$(rrepeat_id).prop('checked', true);
if (event.recurrence && event.recurrence.BYDAY && event.recurrence.FREQ == 'WEEKLY') {
var wdays = event.recurrence.BYDAY.split(',');
$('input.edit-recurrence-weekly-byday').val(wdays);
}
if (event.recurrence && event.recurrence.BYMONTHDAY) {
$('input.edit-recurrence-monthly-bymonthday').val(String(event.recurrence.BYMONTHDAY).split(','));
$('input.edit-recurrence-monthly-mode').val(['BYMONTHDAY']);
}
if (event.recurrence && event.recurrence.BYDAY && (event.recurrence.FREQ == 'MONTHLY' || event.recurrence.FREQ == 'YEARLY')) {
var byday, section = event.recurrence.FREQ.toLowerCase();
if ((byday = String(event.recurrence.BYDAY).match(/(-?[1-4])([A-Z]+)/))) {
$('#edit-recurrence-'+section+'-prefix').val(byday[1]);
$('#edit-recurrence-'+section+'-byday').val(byday[2]);
}
$('input.edit-recurrence-'+section+'-mode').val(['BYDAY']);
}
else if (event.start) {
$('#edit-recurrence-monthly-byday').val(weekdays[event.start.getDay()]);
}
if (event.recurrence && event.recurrence.BYMONTH) {
$('input.edit-recurrence-yearly-bymonth').val(String(event.recurrence.BYMONTH).split(','));
}
else if (event.start) {
$('input.edit-recurrence-yearly-bymonth').val([String(event.start.getMonth()+1)]);
}
};
// show warning if editing a recurring event
if (event.id && event.recurrence) {
$('#edit-recurring-warning').show();
$('input.edit-recurring-savemode[value="all"]').prop('checked', true);
}
else
$('#edit-recurring-warning').hide();
// init attendees tab
var organizer = !event.attendees || is_organizer(event);
event_attendees = [];
attendees_list = $('#edit-attendees-table > tbody').html('');
$('#edit-attendees-notify')[(notify.checked && organizer ? 'show' : 'hide')]();
$('#edit-localchanges-warning')[(has_attendees(event) && !organizer ? 'show' : 'hide')]();
var load_attendees_tab = function()
{
if (event.attendees) {
for (var j=0; j < event.attendees.length; j++)
add_attendee(event.attendees[j], !organizer);
}
$('#edit-attendees-form')[(organizer?'show':'hide')]();
$('#edit-attendee-schedule')[(calendar.freebusy?'show':'hide')]();
};
// attachments
var load_attachments_tab = function()
{
rcmail.enable_command('remove-attachment', !calendar.readonly);
rcmail.env.deleted_attachments = [];
// we're sharing some code for uploads handling with app.js
rcmail.env.attachments = [];
rcmail.env.compose_id = event.id; // for rcmail.async_upload_form()
if ($.isArray(event.attachments)) {
event_show_attachments(event.attachments, $('#edit-attachments'), event, true);
}
else {
$('#edit-attachments > ul').empty();
// fetch attachments, some drivers doesn't set 'attachments' array for event?
}
};
// init dialog buttons
var buttons = {};
buttons[rcmail.gettext('save', 'calendar')] = function() {
var start = parse_datetime(allday.checked ? '12:00' : starttime.val(), startdate.val());
var end = parse_datetime(allday.checked ? '13:00' : endtime.val(), enddate.val());
// basic input validatetion
if (start.getTime() > end.getTime()) {
alert(rcmail.gettext('invalideventdates', 'calendar'));
return false;
}
// post data to server
var data = {
calendar: event.calendar,
start: date2unixtime(start),
end: date2unixtime(end),
allday: allday.checked?1:0,
title: title.val(),
description: description.val(),
location: location.val(),
categories: categories.val(),
free_busy: freebusy.val(),
priority: priority.val(),
sensitivity: sensitivity.val(),
recurrence: '',
alarms: '',
attendees: event_attendees,
deleted_attachments: rcmail.env.deleted_attachments,
attachments: []
};
// serialize alarm settings
// TODO: support multiple alarm entries
var alarm = $('select.edit-alarm-type').val();
if (alarm) {
var val, offset = $('select.edit-alarm-offset').val();
if (offset == '@')
data.alarms = '@' + date2unixtime(parse_datetime($('input.edit-alarm-time').val(), $('input.edit-alarm-date').val())) + ':' + alarm;
else if ((val = parseInt($('input.edit-alarm-value').val())) && !isNaN(val) && val >= 0)
data.alarms = offset[0] + val + offset[1] + ':' + alarm;
}
// uploaded attachments list
for (var i in rcmail.env.attachments)
if (i.match(/^rcmfile(.+)/))
data.attachments.push(RegExp.$1);
// read attendee roles
$('select.edit-attendee-role').each(function(i, elem){
if (data.attendees[i])
data.attendees[i].role = $(elem).val();
});
// don't submit attendees if only myself is added as organizer
if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && data.attendees[0].email == settings.identity.email)
data.attendees = [];
// tell server to send notifications
if (data.attendees.length && organizer && ((event.id && notify.checked) || (!event.id && invite.checked))) {
data._notify = 1;
}
// gather recurrence settings
var freq;
if ((freq = recurrence.val()) != '') {
data.recurrence = {
FREQ: freq,
INTERVAL: $('#edit-recurrence-interval-'+freq.toLowerCase()).val()
};
var until = $('input.edit-recurrence-until:checked').val();
if (until == 'count')
data.recurrence.COUNT = rrtimes.val();
else if (until == 'until')
data.recurrence.UNTIL = date2unixtime(parse_datetime(endtime.val(), rrenddate.val()));
if (freq == 'WEEKLY') {
var byday = [];
$('input.edit-recurrence-weekly-byday:checked').each(function(){ byday.push(this.value); });
if (byday.length)
data.recurrence.BYDAY = byday.join(',');
}
else if (freq == 'MONTHLY') {
var mode = $('input.edit-recurrence-monthly-mode:checked').val(), bymonday = [];
if (mode == 'BYMONTHDAY') {
$('input.edit-recurrence-monthly-bymonthday:checked').each(function(){ bymonday.push(this.value); });
if (bymonday.length)
data.recurrence.BYMONTHDAY = bymonday.join(',');
}
else
data.recurrence.BYDAY = $('#edit-recurrence-monthly-prefix').val() + $('#edit-recurrence-monthly-byday').val();
}
else if (freq == 'YEARLY') {
var byday, bymonth = [];
$('input.edit-recurrence-yearly-bymonth:checked').each(function(){ bymonth.push(this.value); });
if (bymonth.length)
data.recurrence.BYMONTH = bymonth.join(',');
if ((byday = $('#edit-recurrence-yearly-byday').val()))
data.recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday;
}
}
data.calendar = calendars.val();
if (event.id) {
data.id = event.id;
if (event.recurrence)
data._savemode = $('input.edit-recurring-savemode:checked').val();
if (data.calendar && data.calendar != event.calendar)
data._fromcalendar = event.calendar;
}
update_event(action, data);
$dialog.dialog("close");
};
if (event.id) {
buttons[rcmail.gettext('remove', 'calendar')] = function() {
me.delete_event(event);
$dialog.dialog('close');
};
}
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
// show/hide tabs according to calendar's feature support
$('#edit-tab-attendees')[(calendar.attendees?'show':'hide')]();
$('#edit-tab-attachments')[(calendar.attachments?'show':'hide')]();
// activate the first tab
$('#eventtabs').tabs('select', 0);
// hack: set task to 'calendar' to make all dialog actions work correctly
var comm_path_before = rcmail.env.comm_path;
rcmail.env.comm_path = comm_path_before.replace(/_task=[a-z]+/, '_task=calendar');
var editform = $("#eventedit");
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: (!bw.ie6 && !bw.ie7), // disable for performance reasons
closeOnEscape: false,
title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'),
close: function() {
editform.hide().appendTo(document.body);
$dialog.dialog("destroy").remove();
rcmail.ksearch_blur();
rcmail.ksearch_destroy();
freebusy_data = {};
rcmail.env.comm_path = comm_path_before; // restore comm_path
},
buttons: buttons,
minWidth: 500,
width: 580
}).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE6
// set dialog size according to form content
me.dialog_resize($dialog.get(0), editform.height() + (bw.ie ? 20 : 0), 530);
title.select();
// init other tabs asynchronously
window.setTimeout(load_recurrence_tab, exec_deferred);
if (calendar.attendees)
window.setTimeout(load_attendees_tab, exec_deferred);
if (calendar.attachments)
window.setTimeout(load_attachments_tab, exec_deferred);
};
// open a dialog to display detailed free-busy information and to find free slots
var event_freebusy_dialog = function()
{
var $dialog = $('#eventfreebusy').dialog('close');
var event = me.selected_event;
if (!event_attendees.length)
return false;
// set form elements
var allday = $('#edit-allday').get(0);
var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
freebusy_ui.startdate = $('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
freebusy_ui.starttime = $('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
freebusy_ui.enddate = $('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
freebusy_ui.endtime = $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
if (allday.checked) {
freebusy_ui.starttime.val("12:00").hide();
freebusy_ui.endtime.val("13:00").hide();
event.allDay = true;
}
// read attendee roles from drop-downs
$('select.edit-attendee-role').each(function(i, elem){
if (event_attendees[i])
event_attendees[i].role = $(elem).val();
});
// render time slots
var now = new Date(), fb_start = new Date(), fb_end = new Date();
fb_start.setTime(event.start);
fb_start.setHours(0); fb_start.setMinutes(0); fb_start.setSeconds(0); fb_start.setMilliseconds(0);
fb_end.setTime(fb_start.getTime() + DAY_MS);
freebusy_data = { required:{}, all:{} };
freebusy_ui.loading = 1; // prevent render_freebusy_grid() to load data yet
freebusy_ui.numdays = Math.max(allday.checked ? 14 : 1, Math.ceil(duration * 2 / 86400));
freebusy_ui.interval = allday.checked ? 1440 : 60;
freebusy_ui.start = fb_start;
freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays);
render_freebusy_grid(0);
// render list of attendees
freebusy_ui.attendees = {};
var domid, dispname, data, role_html, list_html = '';
for (var i=0; i < event_attendees.length; i++) {
data = event_attendees[i];
dispname = Q(data.name || data.email);
domid = String(data.email).replace(rcmail.identifier_expr, '');
role_html = '<a class="attendee-role-toggle" id="rcmlia' + domid + '" title="' + Q(rcmail.gettext('togglerole', 'calendar')) + '">&nbsp;</a>';
list_html += '<div class="attendee ' + String(data.role).toLowerCase() + '" id="rcmli' + domid + '">' + role_html + dispname + '</div>';
// clone attendees data for local modifications
freebusy_ui.attendees[i] = freebusy_ui.attendees[domid] = $.extend({}, data);
}
// add total row
list_html += '<div class="attendee spacer">&nbsp;</div>';
list_html += '<div class="attendee total">' + rcmail.gettext('reqallattendees','calendar') + '</div>';
$('#schedule-attendees-list').html(list_html)
.unbind('click.roleicons')
.bind('click.roleicons', function(e){
// toggle attendee status upon click on icon
if (e.target.id && e.target.id.match(/rcmlia(.+)/)) {
var attendee, domid = RegExp.$1, roles = [ 'REQ-PARTICIPANT', 'OPT-PARTICIPANT', 'CHAIR' ];
if ((attendee = freebusy_ui.attendees[domid]) && attendee.role != 'ORGANIZER') {
var req = attendee.role != 'OPT-PARTICIPANT';
var j = $.inArray(attendee.role, roles);
j = (j+1) % roles.length;
attendee.role = roles[j];
$(e.target).parent().removeClass().addClass('attendee '+String(attendee.role).toLowerCase());
// update total display if required-status changed
if (req != (roles[j] != 'OPT-PARTICIPANT')) {
compute_freebusy_totals();
update_freebusy_display(attendee.email);
}
}
}
return false;
});
// enable/disable buttons
$('#shedule-find-prev').button('option', 'disabled', (fb_start.getTime() < now.getTime()));
// dialog buttons
var buttons = {};
buttons[rcmail.gettext('select', 'calendar')] = function() {
$('#edit-startdate').val(freebusy_ui.startdate.val());
$('#edit-starttime').val(freebusy_ui.starttime.val());
$('#edit-enddate').val(freebusy_ui.enddate.val());
$('#edit-endtime').val(freebusy_ui.endtime.val());
// write role changes back to main dialog
$('select.edit-attendee-role').each(function(i, elem){
if (event_attendees[i] && freebusy_ui.attendees[i]) {
event_attendees[i].role = freebusy_ui.attendees[i].role;
$(elem).val(event_attendees[i].role);
}
});
if (freebusy_ui.needsupdate)
update_freebusy_status(me.selected_event);
freebusy_ui.needsupdate = false;
$dialog.dialog("close");
};
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: (!bw.ie6 && !bw.ie7),
title: rcmail.gettext('scheduletime', 'calendar'),
close: function() {
if (bw.ie6)
$("#edit-attendees-table").css('visibility','visible');
$dialog.dialog("destroy").hide();
},
resizeStop: function() {
render_freebusy_overlay();
},
buttons: buttons,
minWidth: 640,
width: 850
}).show();
// hide edit dialog on IE6 because of drop-down elements
if (bw.ie6)
$("#edit-attendees-table").css('visibility','hidden');
// adjust dialog size to fit grid without scrolling
var gridw = $('#schedule-freebusy-times').width();
var overflow = gridw - $('#attendees-freebusy-table td.times').width() + 1;
me.dialog_resize($dialog.get(0), $dialog.height() + (bw.ie ? 20 : 0), 800 + Math.max(0, overflow));
// fetch data from server
freebusy_ui.loading = 0;
load_freebusy_data(freebusy_ui.start, freebusy_ui.interval);
};
// render an HTML table showing free-busy status for all the event attendees
var render_freebusy_grid = function(delta)
{
if (delta) {
freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta);
// skip weekends if in workinhoursonly-mode
if (Math.abs(delta) == 1 && freebusy_ui.workinhoursonly) {
while (is_weekend(freebusy_ui.start))
freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta);
}
freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays);
}
var dayslots = Math.floor(1440 / freebusy_ui.interval);
var date_format = 'ddd '+ (dayslots <= 2 ? settings.date_short : settings.date_format);
var lastdate, datestr, css,
curdate = new Date(),
allday = (freebusy_ui.interval == 1440),
times_css = (allday ? 'allday ' : ''),
dates_row = '<tr class="dates">',
times_row = '<tr class="times">',
slots_row = '';
for (var s = 0, t = freebusy_ui.start.getTime(); t < freebusy_ui.end.getTime(); s++) {
curdate.setTime(t);
datestr = fc.fullCalendar('formatDate', curdate, date_format);
if (datestr != lastdate) {
dates_row += '<th colspan="' + dayslots + '" class="boxtitle date' + $.fullCalendar.formatDate(curdate, 'ddMMyyyy') + '">' + Q(datestr) + '</th>';
lastdate = datestr;
}
// set css class according to working hours
css = is_weekend(curdate) || (freebusy_ui.interval <= 60 && !is_workinghour(curdate)) ? 'offhours' : 'workinghours';
times_row += '<td class="' + times_css + css + '" id="t-' + Math.floor(t/1000) + '">' + Q(allday ? rcmail.gettext('all-day','calendar') : $.fullCalendar.formatDate(curdate, settings['time_format'])) + '</td>';
slots_row += '<td class="' + css + ' unknown">&nbsp;</td>';
t += freebusy_ui.interval * 60000;
}
dates_row += '</tr>';
times_row += '</tr>';
// render list of attendees
var domid, data, list_html = '', times_html = '';
for (var i=0; i < event_attendees.length; i++) {
data = event_attendees[i];
domid = String(data.email).replace(rcmail.identifier_expr, '');
times_html += '<tr id="fbrow' + domid + '">' + slots_row + '</tr>';
}
// add line for all/required attendees
times_html += '<tr class="spacer"><td colspan="' + (dayslots * freebusy_ui.numdays) + '">&nbsp;</td>';
times_html += '<tr id="fbrowall">' + slots_row + '</tr>';
var table = $('#schedule-freebusy-times');
table.children('thead').html(dates_row + times_row);
table.children('tbody').html(times_html);
// initialize event handlers on grid
if (!freebusy_ui.grid_events) {
freebusy_ui.grid_events = true;
table.children('thead').click(function(e){
// move event to the clicked date/time
if (e.target.id && e.target.id.match(/t-(\d+)/)) {
var newstart = new Date(RegExp.$1 * 1000);
// set time to 00:00
if (me.selected_event.allDay) {
newstart.setMinutes(0);
newstart.setHours(0);
}
update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
render_freebusy_overlay();
}
})
}
// if we have loaded free-busy data, show it
if (!freebusy_ui.loading) {
if (date2unixtime(freebusy_ui.start) < freebusy_data.start || date2unixtime(freebusy_ui.end) > freebusy_data.end || freebusy_ui.interval != freebusy_data.interval) {
load_freebusy_data(freebusy_ui.start, freebusy_ui.interval);
}
else {
for (var email, i=0; i < event_attendees.length; i++) {
if ((email = event_attendees[i].email))
update_freebusy_display(email);
}
}
}
// render current event date/time selection over grid table
// use timeout to let the dom attributes (width/height/offset) be set first
window.setTimeout(function(){ render_freebusy_overlay(); }, 10);
};
// render overlay element over the grid to visiualize the current event date/time
var render_freebusy_overlay = function()
{
var overlay = $('#schedule-event-time');
if (me.selected_event.end.getTime() <= freebusy_ui.start.getTime() || me.selected_event.start.getTime() >= freebusy_ui.end.getTime()) {
overlay.draggable('disable').hide();
}
else {
var table = $('#schedule-freebusy-times'),
width = 0,
pos = { top:table.children('thead').height(), left:0 },
eventstart = date2unixtime(clone_date(me.selected_event.start, me.selected_event.allDay?1:0)),
eventend = date2unixtime(clone_date(me.selected_event.end, me.selected_event.allDay?2:0)) - 60,
slotstart = date2unixtime(freebusy_ui.start),
slotsize = freebusy_ui.interval * 60,
slotend, fraction, $cell;
// iterate through slots to determine position and size of the overlay
table.children('thead').find('td').each(function(i, cell){
slotend = slotstart + slotsize - 1;
// event starts in this slot: compute left
if (eventstart >= slotstart && eventstart <= slotend) {
fraction = 1 - (slotend - eventstart) / slotsize;
pos.left = Math.round(cell.offsetLeft + cell.offsetWidth * fraction);
}
// event ends in this slot: compute width
if (eventend >= slotstart && eventend <= slotend) {
fraction = 1 - (slotend - eventend) / slotsize;
width = Math.round(cell.offsetLeft + cell.offsetWidth * fraction) - pos.left;
}
slotstart = slotstart + slotsize;
});
if (!width)
width = table.width() - pos.left;
// overlay is visible
if (width > 0) {
overlay.css({ width: (width-5)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).draggable('enable').show();
// configure draggable
if (!overlay.data('isdraggable')) {
overlay.draggable({
axis: 'x',
scroll: true,
stop: function(e, ui){
// convert pixels to time
var px = ui.position.left;
var range_p = $('#schedule-freebusy-times').width();
var range_t = freebusy_ui.end.getTime() - freebusy_ui.start.getTime();
var newstart = new Date(freebusy_ui.start.getTime() + px * (range_t / range_p));
newstart.setSeconds(0); newstart.setMilliseconds(0);
// snap to day boundaries
if (me.selected_event.allDay) {
if (newstart.getHours() >= 12) // snap to next day
newstart.setTime(newstart.getTime() + DAY_MS);
newstart.setMinutes(0);
newstart.setHours(0);
}
else {
// round to 5 minutes
var round = newstart.getMinutes() % 5;
if (round > 2.5) newstart.setTime(newstart.getTime() + (5 - round) * 60000);
else if (round > 0) newstart.setTime(newstart.getTime() - round * 60000);
}
// update event times and display
update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
if (me.selected_event.allDay)
render_freebusy_overlay();
}
}).data('isdraggable', true);
}
}
else
overlay.draggable('disable').hide();
}
};
// fetch free-busy information for each attendee from server
var load_freebusy_data = function(from, interval)
{
var start = new Date(from.getTime() - DAY_MS * 2); // start 1 days before event
var end = new Date(start.getTime() + DAY_MS * Math.max(14, freebusy_ui.numdays + 7)); // load min. 14 days
freebusy_ui.numrequired = 0;
freebusy_data.all = [];
freebusy_data.required = [];
// load free-busy information for every attendee
var domid, email;
for (var i=0; i < event_attendees.length; i++) {
if ((email = event_attendees[i].email)) {
domid = String(email).replace(rcmail.identifier_expr, '');
$('#rcmli' + domid).addClass('loading');
freebusy_ui.loading++;
$.ajax({
type: 'GET',
dataType: 'json',
url: rcmail.url('freebusy-times'),
data: { email:email, start:date2unixtime(clone_date(start, 1)), end:date2unixtime(clone_date(end, 2)), interval:interval, _remote:1 },
success: function(data) {
freebusy_ui.loading--;
// find attendee
var attendee = null;
for (var i=0; i < event_attendees.length; i++) {
if (freebusy_ui.attendees[i].email == data.email) {
attendee = freebusy_ui.attendees[i];
break;
}
}
// copy data to member var
var req = attendee.role != 'OPT-PARTICIPANT';
var ts = data.start - 0;
freebusy_data.start = ts;
freebusy_data[data.email] = {};
for (var i=0; i < data.slots.length; i++) {
freebusy_data[data.email][ts] = data.slots[i];
// set totals
if (!freebusy_data.required[ts])
freebusy_data.required[ts] = [0,0,0,0];
if (req)
freebusy_data.required[ts][data.slots[i]]++;
if (!freebusy_data.all[ts])
freebusy_data.all[ts] = [0,0,0,0];
freebusy_data.all[ts][data.slots[i]]++;
ts += data.interval * 60;
}
freebusy_data.end = ts;
freebusy_data.interval = data.interval;
// hide loading indicator
var domid = String(data.email).replace(rcmail.identifier_expr, '');
$('#rcmli' + domid).removeClass('loading');
// update display
update_freebusy_display(data.email);
}
});
// count required attendees
if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT')
freebusy_ui.numrequired++;
}
}
};
// re-calculate total status after role change
var compute_freebusy_totals = function()
{
freebusy_ui.numrequired = 0;
freebusy_data.all = [];
freebusy_data.required = [];
var email, req, status;
for (var i=0; i < event_attendees.length; i++) {
if (!(email = event_attendees[i].email))
continue;
req = freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT';
if (req)
freebusy_ui.numrequired++;
for (var ts in freebusy_data[email]) {
if (!freebusy_data.required[ts])
freebusy_data.required[ts] = [0,0,0,0];
if (!freebusy_data.all[ts])
freebusy_data.all[ts] = [0,0,0,0];
status = freebusy_data[email][ts];
freebusy_data.all[ts][status]++;
if (req)
freebusy_data.required[ts][status]++;
}
}
};
// update free-busy grid with status loaded from server
var update_freebusy_display = function(email)
{
var status_classes = ['unknown','free','busy','tentative','out-of-office'];
var domid = String(email).replace(rcmail.identifier_expr, '');
var row = $('#fbrow' + domid);
var rowall = $('#fbrowall').children();
var ts = date2unixtime(freebusy_ui.start);
var fbdata = freebusy_data[email];
if (fbdata && fbdata[ts] !== undefined && row.length) {
row.children().each(function(i, cell){
cell.className = cell.className.replace('unknown', fbdata[ts] ? status_classes[fbdata[ts]] : 'unknown');
// also update total row if all data was loaded
if (freebusy_ui.loading == 0 && freebusy_data.all[ts] && (cell = rowall.get(i))) {
var workinghours = cell.className.indexOf('workinghours') >= 0;
var all_status = freebusy_data.all[ts][2] ? 'busy' : 'unknown';
req_status = freebusy_data.required[ts][2] ? 'busy' : 'free';
for (var j=1; j < status_classes.length; j++) {
if (freebusy_ui.numrequired && freebusy_data.required[ts][j] >= freebusy_ui.numrequired)
req_status = status_classes[j];
if (freebusy_data.all[ts][j] == event_attendees.length)
all_status = status_classes[j];
}
cell.className = (workinghours ? 'workinghours ' : 'offhours ') + req_status + ' all-' + all_status;
}
ts += freebusy_ui.interval * 60;
});
}
};
// write changed event date/times back to form fields
var update_freebusy_dates = function(start, end)
{
if (me.selected_event.allDay) {
start.setHours(12);
start.setMinutes(0);
end.setHours(13);
end.setMinutes(0);
}
me.selected_event.start = start;
me.selected_event.end = end;
freebusy_ui.startdate.val($.fullCalendar.formatDate(start, settings['date_format']));
freebusy_ui.starttime.val($.fullCalendar.formatDate(start, settings['time_format']));
freebusy_ui.enddate.val($.fullCalendar.formatDate(end, settings['date_format']));
freebusy_ui.endtime.val($.fullCalendar.formatDate(end, settings['time_format']));
freebusy_ui.needsupdate = true;
};
// attempt to find a time slot where all attemdees are available
var freebusy_find_slot = function(dir)
{
var event = me.selected_event,
eventstart = date2unixtime(event.start), // calculate with unixtimes
eventend = date2unixtime(event.end),
duration = eventend - eventstart,
sinterval = freebusy_data.interval * 60,
intvlslots = 1,
numslots = Math.ceil(duration / sinterval),
checkdate, slotend, email, curdate;
// shift event times to next possible slot
eventstart += sinterval * intvlslots * dir;
eventend += sinterval * intvlslots * dir;
// iterate through free-busy slots and find candidates
var candidatecount = 0, candidatestart = candidateend = success = false;
for (var slot = dir > 0 ? freebusy_data.start : freebusy_data.end - sinterval; (dir > 0 && slot < freebusy_data.end) || (dir < 0 && slot >= freebusy_data.start); slot += sinterval * dir) {
slotend = slot + sinterval;
if ((dir > 0 && slotend <= eventstart) || (dir < 0 && slot >= eventend)) // skip
continue;
// respect workingours setting
if (freebusy_ui.workinhoursonly) {
curdate = fromunixtime(dir > 0 || !candidateend ? slot : (candidateend - duration));
if (is_weekend(curdate) || (freebusy_data.interval <= 60 && !is_workinghour(curdate))) { // skip off-hours
candidatestart = candidateend = false;
candidatecount = 0;
continue;
}
}
if (!candidatestart)
candidatestart = slot;
// check freebusy data for all attendees
for (var i=0; i < event_attendees.length; i++) {
if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT' && (email = freebusy_ui.attendees[i].email) && freebusy_data[email] && freebusy_data[email][slot] > 1) {
candidatestart = candidateend = false;
break;
}
}
// occupied slot
if (!candidatestart) {
slot += Math.max(0, intvlslots - candidatecount - 1) * sinterval * dir;
candidatecount = 0;
continue;
}
// set candidate end to slot end time
candidatecount++;
if (dir < 0 && !candidateend)
candidateend = slotend;
// if candidate is big enough, this is it!
if (candidatecount == numslots) {
if (dir > 0) {
event.start = fromunixtime(candidatestart);
event.end = fromunixtime(candidatestart + duration);
}
else {
event.end = fromunixtime(candidateend);
event.start = fromunixtime(candidateend - duration);
}
success = true;
break;
}
}
// update event date/time display
if (success) {
update_freebusy_dates(event.start, event.end);
// move freebusy grid if necessary
var offset = Math.ceil((event.start.getTime() - freebusy_ui.end.getTime()) / DAY_MS);
if (event.start.getTime() >= freebusy_ui.end.getTime())
render_freebusy_grid(Math.max(1, offset));
else if (event.end.getTime() <= freebusy_ui.start.getTime())
render_freebusy_grid(Math.min(-1, offset));
else
render_freebusy_overlay();
var now = new Date();
$('#shedule-find-prev').button('option', 'disabled', (event.start.getTime() < now.getTime()));
}
else {
alert(rcmail.gettext('noslotfound','calendar'));
}
};
// update event properties and attendees availability if event times have changed
var event_times_changed = function()
{
if (me.selected_event) {
var allday = $('#edit-allday').get(0);
me.selected_event.allDay = allday.checked;
me.selected_event.start = parse_datetime(allday.checked ? '12:00' : $('#edit-starttime').val(), $('#edit-startdate').val());
me.selected_event.end = parse_datetime(allday.checked ? '13:00' : $('#edit-endtime').val(), $('#edit-enddate').val());
if (event_attendees)
freebusy_ui.needsupdate = true;
$('#edit-startdate').data('duration', Math.round((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / 1000));
}
};
// add the given list of participants
var add_attendees = function(names)
{
names = explode_quoted_string(names.replace(/,\s*$/, ''), ',');
// parse name/email pairs
var item, email, name, success = false;
for (var i=0; i < names.length; i++) {
email = name = '';
item = $.trim(names[i]);
if (!item.length) {
continue;
} // address in brackets without name (do nothing)
else if (item.match(/^<[^@]+@[^>]+>$/)) {
email = item.replace(/[<>]/g, '');
} // address without brackets and without name (add brackets)
else if (rcube_check_email(item)) {
email = item;
} // address with name
else if (item.match(/([^\s<@]+@[^>]+)>*$/)) {
email = RegExp.$1;
name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, '');
}
if (email) {
add_attendee({ email:email, name:name, role:'REQ-PARTICIPANT', status:'NEEDS-ACTION' });
success = true;
}
else {
alert(rcmail.gettext('noemailwarning'));
}
}
return success;
};
// add the given attendee to the list
var add_attendee = function(data, readonly)
{
// check for dupes...
var exists = false;
$.each(event_attendees, function(i, v){ exists |= (v.email == data.email); });
if (exists)
return false;
var dispname = Q(data.name || data.email);
if (data.email)
dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
// role selection
var organizer = data.role == 'ORGANIZER';
var opts = {};
if (organizer)
opts.ORGANIZER = rcmail.gettext('calendar.roleorganizer');
opts['REQ-PARTICIPANT'] = rcmail.gettext('calendar.rolerequired');
opts['OPT-PARTICIPANT'] = rcmail.gettext('calendar.roleoptional');
opts['CHAIR'] = rcmail.gettext('calendar.roleresource');
var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + '>';
for (var r in opts)
select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
select += '</select>';
// availability
var avail = data.email ? 'loading' : 'unknown';
// delete icon
var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
var dellink = '<a href="#delete" class="deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
var html = '<td class="role">' + select + '</td>' +
'<td class="name">' + dispname + '</td>' +
'<td class="availability"><img src="./program/blank.gif" class="availabilityicon ' + avail + '" /></td>' +
'<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '">' + Q(data.status) + '</span></td>' +
'<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
var tr = $('<tr>')
.addClass(String(data.role).toLowerCase())
.html(html)
.appendTo(attendees_list);
tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
tr.find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
// check free-busy status
if (avail == 'loading') {
check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_event);
}
event_attendees.push(data);
};
// iterate over all attendees and update their free-busy status display
var update_freebusy_status = function(event)
{
var icons = attendees_list.find('img.availabilityicon');
for (var i=0; i < event_attendees.length; i++) {
if (icons.get(i) && event_attendees[i].email)
check_freebusy_status(icons.get(i), event_attendees[i].email, event);
}
freebusy_ui.needsupdate = false;
};
// load free-busy status from server and update icon accordingly
var check_freebusy_status = function(icon, email, event)
{
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { freebusy:false };
if (!calendar.freebusy) {
$(icon).removeClass().addClass('availabilityicon unknown');
return;
}
icon = $(icon).removeClass().addClass('availabilityicon loading');
$.ajax({
type: 'GET',
dataType: 'html',
url: rcmail.url('freebusy-status'),
data: { email:email, start:date2unixtime(clone_date(event.start, event.allDay?1:0)), end:date2unixtime(clone_date(event.end, event.allDay?2:0)), _remote: 1 },
success: function(status){
icon.removeClass('loading').addClass(String(status).toLowerCase());
},
error: function(){
icon.removeClass('loading').addClass('unknown');
}
});
};
// remove an attendee from the list
var remove_attendee = function(elem, id)
{
$(elem).closest('tr').remove();
event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) });
};
// when the user accepts or declines an event invitation
var event_rsvp = function(response)
{
if (me.selected_event && me.selected_event.attendees && response) {
// update attendee status
for (var data, i=0; i < me.selected_event.attendees.length; i++) {
data = me.selected_event.attendees[i];
if (settings.identity.emails.indexOf(';'+data.email) >= 0)
data.status = response.toUpperCase();
}
event_show_dialog(me.selected_event);
// submit status change to server
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('event', { action:'rsvp', e:me.selected_event, status:response });
}
}
// post the given event data to server
var update_event = function(action, data)
{
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar/event', { action:action, e:data });
// render event temporarily into the calendar
if ((data.start && data.end) || data.id) {
var event = data.id ? $.extend(fc.fullCalendar('clientEvents', data.id)[0], data) : data;
if (data.start)
event.start = fromunixtime(data.start);
if (data.end)
event.end = fromunixtime(data.end);
if (data.allday !== undefined)
event.allDay = data.allday;
event.editable = false;
event.temp = true;
event.className = 'fc-event-cal-'+data.calendar+' fc-event-temp';
fc.fullCalendar(data.id ? 'updateEvent' : 'renderEvent', event);
}
};
// mouse-click handler to check if the show dialog is still open and prevent default action
var dialog_check = function(e)
{
var showd = $("#eventshow");
if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length) {
showd.dialog('close');
e.stopImmediatePropagation();
ignore_click = true;
return false;
}
else if (ignore_click) {
window.setTimeout(function(){ ignore_click = false; }, 20);
return false;
}
return true;
};
// display confirm dialog when modifying/deleting an event
var update_event_confirm = function(action, event, data)
{
if (!data) data = event;
var decline = false, notify = false, html = '', cal = me.calendars[event.calendar];
// event has attendees, ask whether to notify them
if (has_attendees(event)) {
if (is_organizer(event)) {
notify = true;
html += '<div class="message">' +
'<label><input class="confirm-attendees-donotify" type="checkbox" checked="checked" value="1" name="notify" />&nbsp;' +
rcmail.gettext((action == 'remove' ? 'sendcancellation' : 'sendnotifications'), 'calendar') +
'</label></div>';
}
else if (action == 'remove' && is_attendee(event)) {
decline = true;
html += '<div class="message">' +
'<label><input class="confirm-attendees-decline" type="checkbox" checked="checked" value="1" name="decline" />&nbsp;' +
rcmail.gettext('itipdeclineevent', 'calendar') +
'</label></div>';
}
else {
html += '<div class="message">' + rcmail.gettext('localchangeswarning', 'calendar') + '</div>';
}
}
// recurring event: user needs to select the savemode
if (event.recurrence) {
html += '<div class="message"><span class="ui-icon ui-icon-alert"></span>' +
rcmail.gettext((action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning'), 'calendar') + '</div>' +
'<div class="savemode">' +
'<a href="#current" class="button">' + rcmail.gettext('currentevent', 'calendar') + '</a>' +
'<a href="#future" class="button">' + rcmail.gettext('futurevents', 'calendar') + '</a>' +
'<a href="#all" class="button">' + rcmail.gettext('allevents', 'calendar') + '</a>' +
(action != 'remove' ? '<a href="#new" class="button">' + rcmail.gettext('saveasnew', 'calendar') + '</a>' : '') +
'</div>';
}
// show dialog
if (html) {
var $dialog = $('<div>').html(html);
$dialog.find('a.button').button().click(function(e){
data._savemode = String(this.href).replace(/.+#/, '');
if ($dialog.find('input.confirm-attendees-donotify').get(0))
data._notify = notify && $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
if (decline && $dialog.find('input.confirm-attendees-decline:checked'))
data.decline = 1;
update_event(action, data);
$dialog.dialog("destroy").hide();
return false;
});
var buttons = [{
text: rcmail.gettext('cancel', 'calendar'),
click: function() {
$(this).dialog("close");
}
}];
if (!event.recurrence) {
buttons.push({
text: rcmail.gettext((action == 'remove' ? 'remove' : 'save'), 'calendar'),
click: function() {
data._notify = notify && $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
data.decline = decline && $dialog.find('input.confirm-attendees-decline:checked').length ? 1 : 0;
update_event(action, data);
$(this).dialog("close");
}
});
}
$dialog.dialog({
modal: true,
width: 460,
dialogClass: 'warning',
title: rcmail.gettext((action == 'remove' ? 'removeeventconfirm' : 'changeeventconfirm'), 'calendar'),
buttons: buttons,
close: function(){
$dialog.dialog("destroy").hide();
if (!rcmail.busy)
fc.fullCalendar('refetchEvents');
}
}).addClass('event-update-confirm').show();
return false;
}
// show regular confirm box when deleting
else if (action == 'remove' && !cal.undelete) {
if (!confirm(rcmail.gettext('deleteventconfirm', 'calendar')))
return false;
}
// do update
update_event(action, data);
return true;
};
var update_agenda_toolbar = function()
{
$('#agenda-listrange').val(fc.fullCalendar('option', 'listRange'));
$('#agenda-listsections').val(fc.fullCalendar('option', 'listSections'));
}
/*** fullcalendar event handlers ***/
var fc_event_render = function(event, element, view) {
if (view.name != 'list' && view.name != 'table') {
var prefix = event.sensitivity != 0 ? String(sensitivitylabels[event.sensitivity]).toUpperCase()+': ' : '';
element.attr('title', prefix + event.title);
}
if (view.name == 'month') {
if (event_resizing)
return true;
else if (view._suppressed[event.id])
return false;
// limit the number of events displayed
var sday = event.start.getMonth()*100 + event.start.getDate();
var eday = event.end ? event.end.getMonth()*100 + event.end.getDate() : sday;
// increase counter for every day
for (var d = sday; d <= eday; d++) {
if (!view._eventcount[d]) view._eventcount[d] = 1;
else view._eventcount[d]++;
}
if (view._eventcount[sday] >= view._maxevents) {
view._suppressed[event.id] = true;
// register this event to be the last of this day segment
if (!view._morelink[sday]) {
view._morelink[sday] = 1;
view._morelink['e'+event.id] = sday;
}
else {
view._morelink[sday]++;
return false; // suppress event
}
}
}
else {
if (event.location) {
element.find('div.fc-event-title').after('<div class="fc-event-location">@&nbsp;' + Q(event.location) + '</div>');
}
if (event.sensitivity != 0)
element.find('div.fc-event-time').append('<i class="fc-icon-sensitive"></i>');
if (event.recurrence)
element.find('div.fc-event-time').append('<i class="fc-icon-recurring"></i>');
if (event.alarms)
element.find('div.fc-event-time').append('<i class="fc-icon-alarms"></i>');
}
};
/*** public methods ***/
// opens calendar day-view in a popup
this.fisheye_view = function(date)
{
$('#fish-eye-view').dialog('close');
// create list of active event sources
var src, cals = {}, sources = [];
for (var id in this.calendars) {
src = $.extend({}, this.calendars[id]);
src.editable = false;
src.url = null;
src.events = [];
if (src.active) {
cals[id] = src;
sources.push(src);
}
}
// copy events already loaded
var events = fc.fullCalendar('clientEvents');
for (var event, i=0; i< events.length; i++) {
event = events[i];
if (event.source && (src = cals[event.source.id])) {
src.events.push(event);
}
}
var h = $(window).height() - 50;
var dialog = $('<div>')
.attr('id', 'fish-eye-view')
.dialog({
modal: true,
width: 680,
height: h,
title: $.fullCalendar.formatDate(date, 'dddd ' + settings['date_long']),
close: function(){
dialog.dialog("destroy");
me.fisheye_date = null;
}
})
.fullCalendar({
header: { left: '', center: '', right: '' },
height: h - 50,
defaultView: 'agendaDay',
date: date.getDate(),
month: date.getMonth(),
year: date.getFullYear(),
ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone
eventSources: sources,
monthNames : settings['months'],
monthNamesShort : settings['months_short'],
dayNames : settings['days'],
dayNamesShort : settings['days_short'],
firstDay : settings['first_day'],
firstHour : settings['first_hour'],
slotMinutes : 60/settings['timeslots'],
timeFormat: { '': settings['time_format'] },
axisFormat : settings['time_format'],
columnFormat: { day: 'dddd ' + settings['date_short'] },
titleFormat: { day: 'dddd ' + settings['date_long'] },
allDayText: rcmail.gettext('all-day', 'calendar'),
currentTimeIndicator: settings.time_indicator,
eventRender: fc_event_render,
eventClick: function(event) {
event_show_dialog(event);
}
});
this.fisheye_date = date;
};
//public method to show the print dialog.
this.print_calendars = function(view)
{
if (!view) view = fc.fullCalendar('getView').name;
var date = fc.fullCalendar('getDate') || new Date();
var range = fc.fullCalendar('option', 'listRange');
var sections = fc.fullCalendar('option', 'listSections');
var printwin = window.open(rcmail.url('print', { view: view, date: date2unixtime(date), range: range, sections: sections, search: this.search_query }), "rc_print_calendars", "toolbar=no,location=yes,menubar=yes,resizable=yes,scrollbars=yes,width=800");
window.setTimeout(function(){ printwin.focus() }, 50);
};
// public method to bring up the new event dialog
this.add_event = function(templ) {
if (this.selected_calendar) {
var now = new Date();
var date = fc.fullCalendar('getDate');
if (typeof date != 'Date')
date = now;
date.setHours(now.getHours()+1);
date.setMinutes(0);
var end = new Date(date.getTime());
end.setHours(date.getHours()+1);
event_edit_dialog('new', $.extend({ start:date, end:end, allDay:false, calendar:this.selected_calendar }, templ || {}));
}
};
// delete the given event after showing a confirmation dialog
this.delete_event = function(event) {
// show confirm dialog for recurring events, use jquery UI dialog
return update_event_confirm('remove', event, { id:event.id, calendar:event.calendar, attendees:event.attendees });
};
// opens a jquery UI dialog with event properties (or empty for creating a new calendar)
this.calendar_edit_dialog = function(calendar)
{
// close show dialog first
var $dialog = $("#calendarform").dialog('close');
if (!calendar)
calendar = { name:'', color:'cc0000', editable:true, showalarms:true };
var form, name, color, alarms;
$dialog.html(rcmail.get_label('loading'));
$.ajax({
type: 'GET',
dataType: 'html',
url: rcmail.url('calendar'),
data: { action:(calendar.id ? 'form-edit' : 'form-new'), c:{ id:calendar.id } },
success: function(data) {
$dialog.html(data);
// resize and reposition dialog window
form = $('#calendarpropform');
me.dialog_resize('#calendarform', form.height(), form.width());
name = $('#calendar-name').prop('disabled', !calendar.editable).val(calendar.editname || calendar.name);
color = $('#calendar-color').val(calendar.color).miniColors({ value: calendar.color, colorValues:rcmail.env.mscolors });
alarms = $('#calendar-showalarms').prop('checked', calendar.showalarms).get(0);
name.select();
}
});
// dialog buttons
var buttons = {};
buttons[rcmail.gettext('save', 'calendar')] = function() {
// form is not loaded
if (!form || !form.length)
return;
// TODO: do some input validation
if (!name.val() || name.val().length < 2) {
alert(rcmail.gettext('invalidcalendarproperties', 'calendar'));
name.select();
return;
}
// post data to server
var data = form.serializeJSON();
if (data.color)
data.color = data.color.replace(/^#/, '');
if (calendar.id)
data.id = calendar.id;
if (alarms)
data.showalarms = alarms.checked ? 1 : 0;
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar', { action:(calendar.id ? 'edit' : 'new'), c:data });
$dialog.dialog("close");
};
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: false,
title: rcmail.gettext((calendar.id ? 'editcalendar' : 'createcalendar'), 'calendar'),
close: function() {
$dialog.html('').dialog("destroy").hide();
},
buttons: buttons,
minWidth: 400,
width: 420
}).show();
};
this.calendar_remove = function(calendar)
{
if (confirm(rcmail.gettext('deletecalendarconfirm', 'calendar'))) {
rcmail.http_post('calendar', { action:'remove', c:{ id:calendar.id } });
return true;
}
return false;
};
this.calendar_destroy_source = function(id)
{
if (this.calendars[id]) {
fc.fullCalendar('removeEventSource', this.calendars[id]);
$(rcmail.get_folder_li(id, 'rcmlical')).remove();
$('#edit-calendar option[value="'+id+'"]').remove();
delete this.calendars[id];
}
};
// open a dialog to upload an .ics file with events to be imported
this.import_events = function(calendar)
{
// close show dialog first
var $dialog = $("#eventsimport").dialog('close');
var form = rcmail.gui_objects.importform;
$('#event-import-calendar').val(calendar.id);
var buttons = {};
buttons[rcmail.gettext('import', 'calendar')] = function() {
if (form && form.elements._data.value) {
rcmail.async_upload_form(form, 'import_events', function(e) {
rcmail.set_busy(false, null, me.saving_lock);
});
// display upload indicator
me.saving_lock = rcmail.set_busy(true, 'uploading');
}
};
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: false,
closeOnEscape: false,
title: rcmail.gettext('importevents', 'calendar'),
close: function() {
$dialog.dialog("destroy").hide();
},
buttons: buttons,
width: 520
}).show();
};
// callback from server if import succeeded
this.import_success = function(p)
{
$("#eventsimport").dialog('close');
rcmail.set_busy(false, null, me.saving_lock);
rcmail.gui_objects.importform.reset();
if (p.refetch)
this.refresh(p);
};
// refresh the calendar view after saving event data
this.refresh = function(p)
{
var source = me.calendars[p.source];
if (source && (p.refetch || (p.update && !source.active))) {
// activate event source if new event was added to an invisible calendar
if (!source.active) {
source.active = true;
fc.fullCalendar('addEventSource', source);
$('#' + rcmail.get_folder_li(source.id, 'rcmlical').id + ' input').prop('checked', true);
}
else
fc.fullCalendar('refetchEvents', source);
}
// add/update single event object
else if (source && p.update) {
var event = p.update;
event.temp = false;
event.editable = source.editable;
var existing = fc.fullCalendar('clientEvents', event.id);
if (existing.length) {
$.extend(existing[0], event);
fc.fullCalendar('updateEvent', existing[0]);
}
else {
event.source = source; // link with source
fc.fullCalendar('renderEvent', event);
}
// refresh fish-eye view
if (me.fisheye_date)
me.fisheye_view(me.fisheye_date);
}
// remove temp events
fc.fullCalendar('removeEvents', function(e){ return e.temp; });
};
/*** event searching ***/
// execute search
this.quicksearch = function()
{
if (rcmail.gui_objects.qsearchbox) {
var q = rcmail.gui_objects.qsearchbox.value;
if (q != '') {
var id = 'search-'+q;
var sources = [];
if (this._search_message)
rcmail.hide_message(this._search_message);
for (var sid in this.calendars) {
if (this.calendars[sid]) {
this.calendars[sid].url = this.calendars[sid].url.replace(/&q=.+/, '') + '&q='+escape(q);
sources.push(sid);
}
}
id += '@'+sources.join(',');
// ignore if query didn't change
if (this.search_request == id) {
return;
}
// remember current view
else if (!this.search_request) {
this.default_view = fc.fullCalendar('getView').name;
}
this.search_request = id;
this.search_query = q;
// change to list view
fc.fullCalendar('option', 'listSections', 'month')
.fullCalendar('option', 'listRange', Math.max(60, settings['agenda_range']))
.fullCalendar('changeView', 'table');
update_agenda_toolbar();
// refetch events with new url (if not already triggered by changeView)
if (!this.is_loading)
fc.fullCalendar('refetchEvents');
}
else // empty search input equals reset
this.reset_quicksearch();
}
};
// reset search and get back to normal event listing
this.reset_quicksearch = function()
{
$(rcmail.gui_objects.qsearchbox).val('');
if (this._search_message)
rcmail.hide_message(this._search_message);
if (this.search_request) {
// hide bottom links of agenda view
fc.find('.fc-list-content > .fc-listappend').hide();
// restore original event sources and view mode from fullcalendar
fc.fullCalendar('option', 'listSections', settings['agenda_sections'])
.fullCalendar('option', 'listRange', settings['agenda_range']);
update_agenda_toolbar();
for (var sid in this.calendars) {
if (this.calendars[sid])
this.calendars[sid].url = this.calendars[sid].url.replace(/&q=.+/, '');
}
if (this.default_view)
fc.fullCalendar('changeView', this.default_view);
if (!this.is_loading)
fc.fullCalendar('refetchEvents');
this.search_request = this.search_query = null;
}
};
// callback if all sources have been fetched from server
this.events_loaded = function(count)
{
var addlinks, append = '';
// enhance list view when searching
if (this.search_request) {
if (!count) {
this._search_message = rcmail.display_message(rcmail.gettext('searchnoresults', 'calendar'), 'notice');
append = '<div class="message">' + rcmail.gettext('searchnoresults', 'calendar') + '</div>';
}
append += '<div class="fc-bottomlinks formlinks"></div>';
addlinks = true;
}
if (fc.fullCalendar('getView').name == 'table') {
var container = fc.find('.fc-list-content > .fc-listappend');
if (append) {
if (!container.length)
container = $('<div class="fc-listappend"></div>').appendTo(fc.find('.fc-list-content'));
container.html(append).show();
}
else if (container.length)
container.hide();
// add links to adjust search date range
if (addlinks) {
var lc = container.find('.fc-bottomlinks');
$('<a>').attr('href', '#').html(rcmail.gettext('searchearlierdates', 'calendar')).appendTo(lc).click(function(){
fc.fullCalendar('incrementDate', 0, -1, 0);
});
lc.append(" ");
$('<a>').attr('href', '#').html(rcmail.gettext('searchlaterdates', 'calendar')).appendTo(lc).click(function(){
var range = fc.fullCalendar('option', 'listRange');
if (range < 90) {
fc.fullCalendar('option', 'listRange', fc.fullCalendar('option', 'listRange') + 30).fullCalendar('render');
update_agenda_toolbar();
}
else
fc.fullCalendar('incrementDate', 0, 1, 0);
});
}
}
if (this.fisheye_date)
this.fisheye_view(this.fisheye_date);
};
// resize and reposition (center) the dialog window
this.dialog_resize = function(id, height, width)
{
var win = $(window), w = win.width(), h = win.height();
$(id).dialog('option', { height: Math.min(h-20, height+110), width: Math.min(w-20, width+50) })
.dialog('option', 'position', ['center', 'center']); // only works in a separate call (!?)
};
// adjust calendar view size
this.view_resize = function()
{
var footer = fc.fullCalendar('getView').name == 'table' ? $('#agendaoptions').height() + 2 : 0;
fc.fullCalendar('option', 'height', $('#main').height() - footer);
};
/*** startup code ***/
// create list of event sources AKA calendars
this.calendars = {};
var li, cal, active, event_sources = [];
for (var id in rcmail.env.calendars) {
cal = rcmail.env.calendars[id];
this.calendars[id] = $.extend({
url: "./?_task=calendar&_action=load_events&source="+escape(id),
editable: !cal.readonly,
className: 'fc-event-cal-'+id,
id: id
}, cal);
this.calendars[id].color = settings.event_coloring % 2 ? '' : '#' + cal.color;
if ((active = cal.active || false)) {
event_sources.push(this.calendars[id]);
}
// init event handler on calendar list checkbox
if ((li = rcmail.get_folder_li(id, 'rcmlical'))) {
$('#'+li.id+' input').click(function(e){
var id = $(this).data('id');
if (me.calendars[id]) { // add or remove event source on click
var action;
if (this.checked) {
action = 'addEventSource';
me.calendars[id].active = true;
}
else {
action = 'removeEventSource';
me.calendars[id].active = false;
}
// add/remove event source
fc.fullCalendar(action, me.calendars[id]);
rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:me.calendars[id].active?1:0 } });
}
}).data('id', id).get(0).checked = active;
$(li).click(function(e){
var id = $(this).data('id');
rcmail.select_folder(id, 'rcmlical');
rcmail.enable_command('calendar-edit', true);
rcmail.enable_command('calendar-remove', 'events-import', !me.calendars[id].readonly);
me.selected_calendar = id;
})
.dblclick(function(){ me.calendar_edit_dialog(me.calendars[me.selected_calendar]); })
.data('id', id);
}
if (!cal.readonly && !this.selected_calendar) {
this.selected_calendar = id;
rcmail.enable_command('addevent', true);
}
}
// select default calendar
if (settings.default_calendar && this.calendars[settings.default_calendar] && !this.calendars[settings.default_calendar].readonly)
this.selected_calendar = settings.default_calendar;
var viewdate = new Date();
if (rcmail.env.date)
viewdate.setTime(fromunixtime(rcmail.env.date));
// initalize the fullCalendar plugin
var fc = $('#calendar').fullCalendar({
header: {
left: 'prev,next today',
center: 'title',
right: 'agendaDay,agendaWeek,month,table'
},
aspectRatio: 1,
date: viewdate.getDate(),
month: viewdate.getMonth(),
year: viewdate.getFullYear(),
ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone
height: $('#main').height(),
eventSources: event_sources,
monthNames : settings['months'],
monthNamesShort : settings['months_short'],
dayNames : settings['days'],
dayNamesShort : settings['days_short'],
firstDay : settings['first_day'],
firstHour : settings['first_hour'],
slotMinutes : 60/settings['timeslots'],
timeFormat: {
'': settings['time_format'],
agenda: settings['time_format'] + '{ - ' + settings['time_format'] + '}',
list: settings['time_format'] + '{ - ' + settings['time_format'] + '}',
table: settings['time_format'] + '{ - ' + settings['time_format'] + '}'
},
axisFormat : settings['time_format'],
columnFormat: {
month: 'ddd', // Mon
week: 'ddd ' + settings['date_short'], // Mon 9/7
day: 'dddd ' + settings['date_short'], // Monday 9/7
table: settings['date_agenda']
},
titleFormat: {
month: 'MMMM yyyy',
week: settings['dates_long'],
day: 'dddd ' + settings['date_long'],
table: settings['dates_long']
},
listPage: 1, // advance one day in agenda view
listRange: settings['agenda_range'],
listSections: settings['agenda_sections'],
tableCols: ['handle', 'date', 'time', 'title', 'location'],
defaultView: rcmail.env.view || settings['default_view'],
allDayText: rcmail.gettext('all-day', 'calendar'),
buttonText: {
prev: (bw.ie6 ? '&nbsp;&lt;&lt;&nbsp;' : '&nbsp;&#9668;&nbsp;'),
next: (bw.ie6 ? '&nbsp;&gt;&gt;&nbsp;' : '&nbsp;&#9658;&nbsp;'),
today: settings['today'],
day: rcmail.gettext('day', 'calendar'),
week: rcmail.gettext('week', 'calendar'),
month: rcmail.gettext('month', 'calendar'),
table: rcmail.gettext('agenda', 'calendar')
},
listTexts: {
until: rcmail.gettext('until', 'calendar'),
past: rcmail.gettext('pastevents', 'calendar'),
today: rcmail.gettext('today', 'calendar'),
tomorrow: rcmail.gettext('tomorrow', 'calendar'),
thisWeek: rcmail.gettext('thisweek', 'calendar'),
nextWeek: rcmail.gettext('nextweek', 'calendar'),
thisMonth: rcmail.gettext('thismonth', 'calendar'),
nextMonth: rcmail.gettext('nextmonth', 'calendar'),
future: rcmail.gettext('futureevents', 'calendar'),
week: rcmail.gettext('weekofyear', 'calendar')
},
selectable: true,
selectHelper: false,
currentTimeIndicator: settings.time_indicator,
loading: function(isLoading) {
me.is_loading = isLoading;
this._rc_loading = rcmail.set_busy(isLoading, 'loading', this._rc_loading);
// trigger callback
if (!isLoading)
me.events_loaded($(this).fullCalendar('clientEvents').length);
},
// event rendering
eventRender: fc_event_render,
eventAfterRender: function(event, element, view) {
// replace event element with more... link
var sday, overflow, link;
if (view.name == 'month' && (sday = view._morelink['e'+event.id]) && (overflow = view._morelink[sday]) > 1) {
link = $('<div>')
.addClass('fc-event-more')
.html(rcmail.gettext('andnmore', 'calendar').replace('$nr', overflow))
.css({ position:'absolute', left:element.css('left'), top:element.css('top'), width:element.css('width') })
.data('date', new Date(event.start.getTime()))
.click(function(e){ me.fisheye_view($(this).data('date')); });
element.replaceWith(link);
}
},
eventResizeStart: function(event, jsEvent, ui, view) {
event_resizing = event.id;
},
eventResizeStop: function(event, jsEvent, ui, view) {
event_resizing = false;
},
// callback for date range selection
select: function(start, end, allDay, e, view) {
var range_select = (!allDay || start.getDate() != end.getDate())
if (dialog_check(e) && range_select)
event_edit_dialog('new', { start:start, end:end, allDay:allDay, calendar:me.selected_calendar });
if (range_select || ignore_click)
view.calendar.unselect();
},
// callback for clicks in all-day box
dayClick: function(date, allDay, e, view) {
var now = new Date().getTime();
if (now - day_clicked_ts < 400 && day_clicked == date.getTime()) { // emulate double-click on day
var enddate = new Date(); enddate.setTime(date.getTime() + DAY_MS - 60000);
return event_edit_dialog('new', { start:date, end:enddate, allDay:allDay, calendar:me.selected_calendar });
}
if (!ignore_click) {
view.calendar.gotoDate(date);
if (day_clicked && new Date(day_clicked).getMonth() != date.getMonth())
view.calendar.select(date, date, allDay);
}
day_clicked = date.getTime();
day_clicked_ts = now;
},
// callback when a specific event is clicked
eventClick: function(event) {
if (!event.temp)
event_show_dialog(event);
},
// callback when an event was dragged and finally dropped
eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc) {
if (event.end == null || event.end.getTime() < event.start.getTime()) {
event.end = new Date(event.start.getTime() + (allDay ? DAY_MS : HOUR_MS));
}
// moved to all-day section: set times to 12:00 - 13:00
if (allDay && !event.allday) {
event.start.setHours(12);
event.start.setMinutes(0);
event.start.setSeconds(0);
event.end.setHours(13);
event.end.setMinutes(0);
event.end.setSeconds(0);
}
// moved from all-day section: set times to working hours
else if (event.allday && !allDay) {
var newstart = event.start.getTime();
revertFunc(); // revert to get original duration
var numdays = Math.max(1, Math.round((event.end.getTime() - event.start.getTime()) / DAY_MS)) - 1;
event.start = new Date(newstart);
event.end = new Date(newstart + numdays * DAY_MS);
event.end.setHours(settings['work_end'] || 18);
event.end.setMinutes(0);
if (event.end.getTime() < event.start.getTime())
event.end = new Date(newstart + HOUR_MS);
}
// send move request to server
var data = {
id: event.id,
calendar: event.calendar,
start: date2unixtime(event.start),
end: date2unixtime(event.end),
allday: allDay?1:0
};
update_event_confirm('move', event, data);
},
// callback for event resizing
eventResize: function(event, delta) {
// sanitize event dates
if (event.allDay)
event.start.setHours(12);
if (!event.end || event.end.getTime() < event.start.getTime())
event.end = new Date(event.start.getTime() + HOUR_MS);
// send resize request to server
var data = {
id: event.id,
calendar: event.calendar,
start: date2unixtime(event.start),
end: date2unixtime(event.end)
};
update_event_confirm('resize', event, data);
},
viewDisplay: function(view) {
$('#agendaoptions')[view.name == 'table' ? 'show' : 'hide']();
if (minical) {
window.setTimeout(function(){ minical.datepicker('setDate', fc.fullCalendar('getDate')); }, exec_deferred);
if (view.name != current_view)
me.view_resize();
current_view = view.name;
}
},
viewRender: function(view) {
view._maxevents = Math.floor((view.element.parent().height()-18) / 108) - 1;
view._eventcount = [];
view._suppressed = [];
view._morelink = [];
}
});
// format time string
var formattime = function(hour, minutes, start) {
var time, diff, unit, duration = '', d = new Date();
d.setHours(hour);
d.setMinutes(minutes);
time = $.fullCalendar.formatDate(d, settings['time_format']);
if (start) {
diff = Math.floor((d.getTime() - start.getTime()) / 60000);
if (diff > 0) {
unit = 'm';
if (diff >= 60) {
unit = 'h';
diff = Math.round(diff / 3) / 20;
}
duration = ' (' + diff + unit + ')';
}
}
return [time, duration];
};
var autocomplete_times = function(p, callback) {
/* Time completions */
var result = [];
var now = new Date();
var st, start = (this.element.attr('id').indexOf('endtime') > 0
&& (st = $('#edit-starttime').val())
&& $('#edit-startdate').val() == $('#edit-enddate').val())
? parse_datetime(st, '') : null;
var full = p.term - 1 > 0 || p.term.length > 1;
var hours = start ? start.getHours() :
(full ? parse_datetime(p.term, '') : now).getHours();
var step = 15;
var minutes = hours * 60 + (full ? 0 : now.getMinutes());
var min = Math.ceil(minutes / step) * step % 60;
var hour = Math.floor(Math.ceil(minutes / step) * step / 60);
// list hours from 0:00 till now
for (var h = start ? start.getHours() : 0; h < hours; h++)
result.push(formattime(h, 0, start));
// list 15min steps for the next two hours
for (; h < hour + 2 && h < 24; h++) {
while (min < 60) {
result.push(formattime(h, min, start));
min += step;
}
min = 0;
}
// list the remaining hours till 23:00
while (h < 24)
result.push(formattime((h++), 0, start));
return callback(result);
};
var autocomplete_open = function(event, ui) {
// scroll to current time
var $this = $(this);
var widget = $this.autocomplete('widget');
var menu = $this.data('autocomplete').menu;
var amregex = /^(.+)(a[.m]*)/i;
var pmregex = /^(.+)(a[.m]*)/i;
var val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1');
var li, html;
widget.css('width', '10em');
widget.children().each(function(){
li = $(this);
html = li.children().first().html().replace(/\s+\(.+\)$/, '').replace(amregex, '0:$1').replace(pmregex, '1:$1');
if (html == val)
menu.activate($.Event({ type:'keypress' }), li);
});
};
// if start date is changed, shift end date according to initial duration
var shift_enddate = function(dateText) {
var newstart = parse_datetime('0', dateText);
var newend = new Date(newstart.getTime() + $('#edit-startdate').data('duration') * 1000);
$('#edit-enddate').val($.fullCalendar.formatDate(newend, me.settings['date_format']));
event_times_changed();
};
var minical;
var init_calendar_ui = function()
{
// initialize small calendar widget using jQuery UI datepicker
minical = $('#datepicker').datepicker($.extend(datepicker_settings, {
inline: true,
showWeek: true,
changeMonth: false, // maybe enable?
changeYear: false, // maybe enable?
onSelect: function(dateText, inst) {
ignore_click = true;
var d = minical.datepicker('getDate'); //parse_datetime('0:0', dateText);
fc.fullCalendar('gotoDate', d).fullCalendar('select', d, d, true);
},
onChangeMonthYear: function(year, month, inst) {
minical.data('year', year).data('month', month);
},
beforeShowDay: function(date) {
var view = fc.fullCalendar('getView');
var active = view.visStart && date.getTime() >= view.visStart.getTime() && date.getTime() < view.visEnd.getTime();
return [ true, (active ? 'ui-datepicker-activerange ui-datepicker-active-' + view.name : ''), ''];
}
})) // set event handler for clicks on calendar week cell of the datepicker widget
.click(function(e) {
var cell = $(e.target);
if (e.target.tagName == 'TD' && cell.hasClass('ui-datepicker-week-col')) {
var base_date = minical.datepicker('getDate');
if (minical.data('month'))
base_date.setMonth(minical.data('month')-1);
if (minical.data('year'))
base_date.setYear(minical.data('year'));
base_date.setHours(12);
var day_off = base_date.getDay() - 1;
if (day_off < 0) day_off = 6;
var base_kw = $.datepicker.iso8601Week(base_date);
var kw = parseInt(cell.html());
var diff = (kw - base_kw) * 7 * DAY_MS;
// select monday of the chosen calendar week
var date = new Date(base_date.getTime() - day_off * DAY_MS + diff);
fc.fullCalendar('gotoDate', date).fullCalendar('setDate', date).fullCalendar('changeView', 'agendaWeek');
minical.datepicker('setDate', date);
}
});
// init event dialog
$('#eventtabs').tabs({
show: function(event, ui) {
if (ui.panel.id == 'event-tab-3') {
$('#edit-attendee-name').select();
// update free-busy status if needed
if (freebusy_ui.needsupdate && me.selected_event)
update_freebusy_status(me.selected_event);
// add current user as organizer if non added yet
if (!event_attendees.length) {
add_attendee($.extend({ role:'ORGANIZER' }, settings.identity));
$('#edit-attendees-form .attendees-invitebox').show();
}
}
}
});
$('#edit-enddate, input.edit-alarm-date').datepicker(datepicker_settings);
$('#edit-startdate').datepicker(datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); });
$('#edit-enddate').datepicker('option', 'onSelect', event_times_changed).change(event_times_changed);
$('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); event_times_changed(); });
// configure drop-down menu on time input fields based on jquery UI autocomplete
$('#edit-starttime, #edit-endtime, input.edit-alarm-time')
.attr('autocomplete', "off")
.autocomplete({
delay: 100,
minLength: 1,
source: autocomplete_times,
open: autocomplete_open,
change: event_times_changed,
select: function(event, ui) {
$(this).val(ui.item[0]);
return false;
}
})
.click(function() { // show drop-down upon clicks
$(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " ");
}).each(function(){
$(this).data('autocomplete')._renderItem = function(ul, item) {
return $('<li>')
.data('item.autocomplete', item)
.append('<a>' + item[0] + item[1] + '</a>')
.appendTo(ul);
};
});
// register events on alarm fields
$('select.edit-alarm-type').change(function(){
$(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')]();
});
$('select.edit-alarm-offset').change(function(){
var mode = $(this).val() == '@' ? 'show' : 'hide';
$(this).parent().find('.edit-alarm-date, .edit-alarm-time')[mode]();
$(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show');
});
// toggle recurrence frequency forms
$('#edit-recurrence-frequency').change(function(e){
var freq = $(this).val().toLowerCase();
$('.recurrence-form').hide();
if (freq)
$('#recurrence-form-'+freq+', #recurrence-form-until').show();
});
$('#edit-recurrence-enddate').datepicker(datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) });
$('#edit-recurrence-repeat-times').change(function(e){ $('#edit-recurrence-repeat-count').prop('checked', true); });
// init attendees autocompletion
var ac_props;
// parallel autocompletion
if (rcmail.env.autocomplete_threads > 0) {
ac_props = {
threads: rcmail.env.autocomplete_threads,
sources: rcmail.env.autocomplete_sources
};
}
rcmail.init_address_input_events($('#edit-attendee-name'), ac_props);
rcmail.addEventListener('autocomplete_insert', function(e){ $('#edit-attendee-add').click(); });
$('#edit-attendee-add').click(function(){
var input = $('#edit-attendee-name');
rcmail.ksearch_blur();
if (add_attendees(input.val())) {
input.val('');
}
});
// keep these two checkboxes in sync
$('#edit-attendees-donotify, #edit-attendees-invite').click(function(){
$('#edit-attendees-donotify, #edit-attendees-invite').prop('checked', this.checked);
});
$('#edit-attendee-schedule').click(function(){
event_freebusy_dialog();
});
$('#shedule-freebusy-prev').html(bw.ie6 ? '&lt;&lt;' : '&#9668;').button().click(function(){ render_freebusy_grid(-1); });
$('#shedule-freebusy-next').html(bw.ie6 ? '&gt;&gt;' : '&#9658;').button().click(function(){ render_freebusy_grid(1); }).parent().buttonset();
$('#shedule-find-prev').button().click(function(){ freebusy_find_slot(-1); });
$('#shedule-find-next').button().click(function(){ freebusy_find_slot(1); });
$('#schedule-freebusy-workinghours').click(function(){
freebusy_ui.workinhoursonly = this.checked;
$('#workinghourscss').remove();
if (this.checked)
$('<style type="text/css" id="workinghourscss"> td.offhours { opacity:0.3; filter:alpha(opacity=30) } </style>').appendTo('head');
});
$('#event-rsvp input.button').click(function(){
event_rsvp($(this).attr('rel'))
})
$('#agenda-listrange').change(function(e){
settings['agenda_range'] = parseInt($(this).val());
fc.fullCalendar('option', 'listRange', settings['agenda_range']).fullCalendar('render');
// TODO: save new settings in prefs
}).val(settings['agenda_range']);
$('#agenda-listsections').change(function(e){
settings['agenda_sections'] = $(this).val();
fc.fullCalendar('option', 'listSections', settings['agenda_sections']).fullCalendar('render');
// TODO: save new settings in prefs
}).val(fc.fullCalendar('option', 'listSections'));
// hide event dialog when clicking somewhere into document
$(document).bind('mousedown', dialog_check);
rcmail.set_busy(false, 'loading', ui_loading);
}
// initialize more UI elements (deferred)
window.setTimeout(init_calendar_ui, exec_deferred);
// add proprietary css styles if not IE
if (!bw.ie)
$('div.fc-content').addClass('rcube-fc-content');
// IE supresses 2nd click event when double-clicking
if (bw.ie && bw.vendver < 9) {
$('div.fc-content').bind('dblclick', function(e){
if (!$(this).hasClass('fc-widget-header') && fc.fullCalendar('getView').name != 'table') {
var date = fc.fullCalendar('getDate');
var enddate = new Date(); enddate.setTime(date.getTime() + DAY_MS - 60000);
event_edit_dialog('new', { start:date, end:enddate, allDay:true, calendar:me.selected_calendar });
}
});
}
} // end rcube_calendar class
/* calendar plugin initialization */
window.rcmail && rcmail.addEventListener('init', function(evt) {
// configure toolbar buttons
rcmail.register_command('addevent', function(){ cal.add_event(); }, true);
rcmail.register_command('print', function(){ cal.print_calendars(); }, true);
// configure list operations
rcmail.register_command('calendar-create', function(){ cal.calendar_edit_dialog(null); }, true);
rcmail.register_command('calendar-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('events-import', function(){ cal.import_events(cal.calendars[cal.selected_calendar]); }, false);
// search and export events
rcmail.register_command('export', function(){ rcmail.goto_url('export_events', { source:cal.selected_calendar }); }, true);
rcmail.register_command('search', function(){ cal.quicksearch(); }, true);
rcmail.register_command('reset-search', function(){ cal.reset_quicksearch(); }, true);
// register callback commands
rcmail.addEventListener('plugin.display_alarms', function(alarms){ cal.display_alarms(alarms); });
rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); });
rcmail.addEventListener('plugin.unlock_saving', function(p){ rcmail.set_busy(false, null, cal.saving_lock); });
rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); });
rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); });
// let's go
var cal = new rcube_calendar_ui(rcmail.env.calendar_settings);
$(window).resize(function(e) {
// check target due to bugs in jquery
// http://bugs.jqueryui.com/ticket/7514
// http://bugs.jquery.com/ticket/9841
if (e.target == window) {
cal.view_resize();
}
}).resize();
// show calendars list when ready
$('#calendars').css('visibility', 'inherit');
// show toolbar
$('#toolbar').show();
});
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 957cfc62..ed641b09 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -1,365 +1,365 @@
<?php
/**
* Driver interface for the Calendar plugin
*
* @version 0.7-beta
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <roundcube@gmail.com>
*
- * Copyright (C) 2010, Lazlo Westerhof - Netherlands
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Struct of an internal event object how it is passed from/to the driver classes:
*
* $event = array(
* 'id' => 'Event ID used for editing',
* 'uid' => 'Unique identifier of this event',
* 'calendar' => 'Calendar identifier to add event to or where the event is stored',
* 'start' => <unixtime>, // Event start date/time as unix timestamp
* 'end' => <unixtime>, // Event end date/time as unix timestamp
* 'allday' => true|false, // Boolean flag if this is an all-day event
* 'changed' => <unixtime>, // Last modification date of event
* 'title' => 'Event title/summary',
* 'location' => 'Location string',
* 'description' => 'Event description',
* 'recurrence' => array( // Recurrence definition according to iCalendar (RFC 2445) specification as list of key-value pairs
* 'FREQ' => 'DAILY|WEEKLY|MONTHLY|YEARLY',
* 'INTERVAL' => 1...n,
* 'UNTIL' => <unixtime>,
* 'COUNT' => 1..n, // number of times
* // + more properties (see http://www.kanzaki.com/docs/ical/recur.html)
* 'EXDATE' => array(), // list of <unixtime>s of exception Dates/Times
* ),
* 'recurrence_id' => 'ID of the recurrence group', // usually the ID of the starting event
* 'categories' => 'Event category',
* 'free_busy' => 'free|busy|outofoffice|tentative', // Show time as
* 'priority' => 0-9, // Event priority (0=undefined, 1=highest, 9=lowest)
* 'sensitivity' => 0|1|2, // Event sensitivity (0=public, 1=private, 2=confidential)
* 'alarms' => '-15M:DISPLAY', // Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before event)
* 'attachments' => array( // List of attachments
* 'name' => 'File name',
* 'mimetype' => 'Content type',
* 'size' => 1..n, // in bytes
* 'id' => 'Attachment identifier'
* ),
* 'deleted_attachments' => array(), // array of attachment identifiers to delete when event is updated
* 'attendees' => array( // List of event participants
* 'name' => 'Participant name',
* 'email' => 'Participant e-mail address', // used as identifier
* 'role' => 'ORGANIZER|REQ-PARTICIPANT|OPT-PARTICIPANT|CHAIR',
* 'status' => 'NEEDS-ACTION|UNKNOWN|ACCEPTED|TENTATIVE|DECLINED'
* 'rsvp' => true|false,
* ),
*
* '_savemode' => 'all|future|current|new', // How changes on recurring event should be handled
* '_notify' => true|false, // whether to notify event attendees about changes
* '_fromcalendar' => 'Calendar identifier where the event was stored before',
* );
*/
/**
* Interface definition for calendar driver classes
*/
abstract class calendar_driver
{
// features supported by backend
public $alarms = false;
public $attendees = false;
public $freebusy = false;
public $attachments = false;
public $undelete = false; // event undelete action
public $categoriesimmutable = false;
public $alarm_types = array('DISPLAY');
public $last_error;
/**
* Get a list of available calendars from this source
*/
abstract function list_calendars();
/**
* Create a new calendar assigned to the current user
*
* @param array Hash array with calendar properties
* name: Calendar name
* color: The color of the calendar
* showalarms: True if alarms are enabled
* @return mixed ID of the calendar on success, False on error
*/
abstract function create_calendar($prop);
/**
* Update properties of an existing calendar
*
* @param array Hash array with calendar properties
* id: Calendar Identifier
* name: Calendar name
* color: The color of the calendar
* showalarms: True if alarms are enabled (if supported)
* @return boolean True on success, Fales on failure
*/
abstract function edit_calendar($prop);
/**
* Set active/subscribed state of a calendar
*
* @param array Hash array with calendar properties
* id: Calendar Identifier
* active: True if calendar is active, false if not
* @return boolean True on success, Fales on failure
*/
abstract function subscribe_calendar($prop);
/**
* Delete the given calendar with all its contents
*
* @param array Hash array with calendar properties
* id: Calendar Identifier
* @return boolean True on success, Fales on failure
*/
abstract function remove_calendar($prop);
/**
* Add a single event to the database
*
* @param array Hash array with event properties (see header of this file)
* @return mixed New event ID on success, False on error
*/
abstract function new_event($event);
/**
* Update an event entry with the given data
*
* @param array Hash array with event properties (see header of this file)
* @return boolean True on success, False on error
*/
abstract function edit_event($event);
/**
* Move a single event
*
* @param array Hash array with event properties:
* id: Event identifier
* start: Event start date/time as unix timestamp
* end: Event end date/time as unix timestamp
* allday: Boolean flag if this is an all-day event
* @return boolean True on success, False on error
*/
abstract function move_event($event);
/**
* Resize a single event
*
* @param array Hash array with event properties:
* id: Event identifier
* start: Event start date/time as unix timestamp in user timezone
* end: Event end date/time as unix timestamp in user timezone
* @return boolean True on success, False on error
*/
abstract function resize_event($event);
/**
* Remove a single event from the database
*
* @param array Hash array with event properties:
* id: Event identifier
* @param boolean Remove event irreversible (mark as deleted otherwise,
* if supported by the backend)
*
* @return boolean True on success, False on error
*/
abstract function remove_event($event, $force = true);
/**
* Restores a single deleted event (if supported)
*
* @param array Hash array with event properties:
* id: Event identifier
*
* @return boolean True on success, False on error
*/
public function restore_event($event)
{
return false;
}
/**
* Return data of a single event
*
* @param mixed UID string or hash array with event properties:
* id: Event identifier
* calendar: Calendar identifier (optional)
* @param boolean If true, only writeable calendars shall be searched
* @return array Event object as hash array
*/
abstract function get_event($event, $writeable = null);
/**
* Get events from source.
*
* @param integer Event's new start (unix timestamp)
* @param integer Event's new end (unix timestamp)
* @param string Search query (optional)
* @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
* @return array A list of event objects (see header of this file for struct of an event)
*/
abstract function load_events($start, $end, $query = null, $calendars = null);
/**
* Get a list of pending alarms to be displayed to the user
*
* @param integer Current time (unix timestamp)
* @param mixed List of calendar IDs to show alarms for (either as array or comma-separated string)
* @return array A list of alarms, each encoded as hash array:
* id: Event identifier
* uid: Unique identifier of this event
* start: Event start date/time as unix timestamp
* end: Event end date/time as unix timestamp
* allday: Boolean flag if this is an all-day event
* title: Event title/summary
* location: Location string
*/
abstract function pending_alarms($time, $calendars = null);
/**
* (User) feedback after showing an alarm notification
* This should mark the alarm as 'shown' or snooze it for the given amount of time
*
* @param string Event identifier
* @param integer Suspend the alarm for this number of seconds
*/
abstract function dismiss_alarm($event_id, $snooze = 0);
/**
* Get list of event's attachments.
* Drivers can return list of attachments as event property.
* If they will do not do this list_attachments() method will be used.
*
* @param array $event Hash array with event properties:
* id: Event identifier
* calendar: Calendar identifier
*
* @return array List of attachments, each as hash array:
* id: Attachment identifier
* name: Attachment name
* mimetype: MIME content type of the attachment
* size: Attachment size
*/
public function list_attachments($event) { }
/**
* Get attachment properties
*
* @param string $id Attachment identifier
* @param array $event Hash array with event properties:
* id: Event identifier
* calendar: Calendar identifier
*
* @return array Hash array with attachment properties:
* id: Attachment identifier
* name: Attachment name
* mimetype: MIME content type of the attachment
* size: Attachment size
*/
public function get_attachment($id, $event) { }
/**
* Get attachment body
*
* @param string $id Attachment identifier
* @param array $event Hash array with event properties:
* id: Event identifier
* calendar: Calendar identifier
*
* @return string Attachment body
*/
public function get_attachment_body($id, $event) { }
/**
* List availabale categories
* The default implementation reads them from config/user prefs
*/
public function list_categories()
{
$rcmail = rcmail::get_instance();
return $rcmail->config->get('calendar_categories', array());
}
/**
* Create a new category
*/
public function add_category($name, $color) { }
/**
* Remove the given category
*/
public function remove_category($name) { }
/**
* Update/replace a category
*/
public function replace_category($oldname, $name, $color) { }
/**
* Fetch free/busy information from a person within the given range
*
* @param string E-mail address of attendee
* @param integer Requested period start date/time as unix timestamp
* @param integer Requested period end date/time as unix timestamp
*
* @return array List of busy timeslots within the requested range
*/
public function get_freebusy_list($email, $start, $end)
{
return false;
}
/**
* Callback function to produce driver-specific calendar create/edit form
*
* @param string Request action 'form-edit|form-new'
* @param array Calendar properties (e.g. id, color)
* @param array Edit form fields
*
* @return string HTML content of the form
*/
public function calendar_form($action, $calendar, $formfields)
{
$html = '';
foreach ($formfields as $prop => $field) {
$html .= html::div('form-section',
html::label($field['id'], $field['label']) .
$field['value']);
}
return $html;
}
/**
* Return a (limited) list of color values to be used for calendar and category coloring
*
* @return mixed List for colors as hex values or false if no presets should be shown
*/
public function get_color_values()
{
return false;
}
}
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index f233d5a2..86e36087 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -1,974 +1,974 @@
<?php
/**
* Database driver for the Calendar plugin
*
* @version 0.7-beta
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <roundcube@gmail.com>
*
- * Copyright (C) 2010, Lazlo Westerhof - Netherlands
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class database_driver extends calendar_driver
{
// features this backend supports
public $alarms = true;
public $attendees = true;
public $freebusy = false;
public $attachments = true;
public $alarm_types = array('DISPLAY','EMAIL');
private $rc;
private $cal;
private $cache = array();
private $calendars = array();
private $calendar_ids = '';
private $free_busy_map = array('free' => 0, 'busy' => 1, 'out-of-office' => 2, 'outofoffice' => 2, 'tentative' => 3);
private $db_events = 'events';
private $db_calendars = 'calendars';
private $db_attachments = 'attachments';
private $sequence_events = 'event_ids';
private $sequence_calendars = 'calendar_ids';
private $sequence_attachments = 'attachment_ids';
/**
* Default constructor
*/
public function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
// load library classes
require_once($this->cal->home . '/lib/Horde_Date_Recurrence.php');
// read database config
$this->db_events = $this->rc->config->get('db_table_events', $this->db_events);
$this->db_calendars = $this->rc->config->get('db_table_calendars', $this->db_calendars);
$this->db_attachments = $this->rc->config->get('db_table_attachments', $this->db_attachments);
$this->sequence_events = $this->rc->config->get('db_sequence_events', $this->sequence_events);
$this->sequence_calendars = $this->rc->config->get('db_sequence_calendars', $this->sequence_calendars);
$this->sequence_attachments = $this->rc->config->get('db_sequence_attachments', $this->sequence_attachments);
$this->_read_calendars();
}
/**
* Read available calendars for the current user and store them internally
*/
private function _read_calendars()
{
$hidden = array_filter(explode(',', $this->rc->config->get('hidden_calendars', '')));
if (!empty($this->rc->user->ID)) {
$calendar_ids = array();
$result = $this->rc->db->query(
"SELECT *, calendar_id AS id FROM " . $this->db_calendars . "
WHERE user_id=?
ORDER BY name",
$this->rc->user->ID
);
while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
$arr['showalarms'] = intval($arr['showalarms']);
$arr['active'] = !in_array($arr['id'], $hidden);
$this->calendars[$arr['calendar_id']] = $arr;
$calendar_ids[] = $this->rc->db->quote($arr['calendar_id']);
}
$this->calendar_ids = join(',', $calendar_ids);
}
}
/**
* Get a list of available calendars from this source
*/
public function list_calendars()
{
// attempt to create a default calendar for this user
if (empty($this->calendars)) {
if ($this->create_calendar(array('name' => 'Default', 'color' => 'cc0000')))
$this->_read_calendars();
}
return $this->calendars;
}
/**
* Create a new calendar assigned to the current user
*
* @param array Hash array with calendar properties
* name: Calendar name
* color: The color of the calendar
* @return mixed ID of the calendar on success, False on error
*/
public function create_calendar($prop)
{
$result = $this->rc->db->query(
"INSERT INTO " . $this->db_calendars . "
(user_id, name, color, showalarms)
VALUES (?, ?, ?, ?)",
$this->rc->user->ID,
$prop['name'],
$prop['color'],
$prop['showalarms']?1:0
);
if ($result)
return $this->rc->db->insert_id($this->sequence_calendars);
return false;
}
/**
* Update properties of an existing calendar
*
* @see calendar_driver::edit_calendar()
*/
public function edit_calendar($prop)
{
$query = $this->rc->db->query(
"UPDATE " . $this->db_calendars . "
SET name=?, color=?, showalarms=?
WHERE calendar_id=?
AND user_id=?",
$prop['name'],
$prop['color'],
$prop['showalarms']?1:0,
$prop['id'],
$this->rc->user->ID
);
return $this->rc->db->affected_rows($query);
}
/**
* Set active/subscribed state of a calendar
* Save a list of hidden calendars in user prefs
*
* @see calendar_driver::subscribe_calendar()
*/
public function subscribe_calendar($prop)
{
$hidden = array_flip(explode(',', $this->rc->config->get('hidden_calendars', '')));
if ($prop['active'])
unset($hidden[$prop['id']]);
else
$hidden[$prop['id']] = 1;
return $this->rc->user->save_prefs(array('hidden_calendars' => join(',', array_keys($hidden))));
}
/**
* Delete the given calendar with all its contents
*
* @see calendar_driver::remove_calendar()
*/
public function remove_calendar($prop)
{
if (!$this->calendars[$prop['id']])
return false;
// events and attachments will be deleted by foreign key cascade
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_calendars . "
WHERE calendar_id=?",
$prop['id']
);
return $this->rc->db->affected_rows($query);
}
/**
* Add a single event to the database
*
* @param array Hash array with event properties
* @see calendar_driver::new_event()
*/
public function new_event($event)
{
if (!empty($this->calendars)) {
if ($event['calendar'] && !$this->calendars[$event['calendar']])
return false;
if (!$event['calendar'])
$event['calendar'] = reset(array_keys($this->calendars));
$event = $this->_save_preprocess($event);
$query = $this->rc->db->query(sprintf(
"INSERT INTO " . $this->db_events . "
(calendar_id, created, changed, uid, start, end, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, attendees, alarms, notifyat)
VALUES (?, %s, %s, ?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
$this->rc->db->now(),
$this->rc->db->now(),
$this->rc->db->fromunixtime($event['start']),
$this->rc->db->fromunixtime($event['end'])
),
$event['calendar'],
strval($event['uid']),
intval($event['all_day']),
$event['_recurrence'],
strval($event['title']),
strval($event['description']),
strval($event['location']),
strval($event['categories']),
intval($event['free_busy']),
intval($event['priority']),
intval($event['sensitivity']),
$event['attendees'],
$event['alarms'],
$event['notifyat']
);
$event_id = $this->rc->db->insert_id($this->sequence_events);
if ($event_id) {
$event['id'] = $event_id;
// add attachments
if (!empty($event['attachments'])) {
foreach ($event['attachments'] as $attachment) {
$this->add_attachment($attachment, $event_id);
unset($attachment);
}
}
$this->_update_recurring($event);
}
return $event_id;
}
return false;
}
/**
* Update an event entry with the given data
*
* @param array Hash array with event properties
* @see calendar_driver::edit_event()
*/
public function edit_event($event)
{
if (!empty($this->calendars)) {
$update_master = false;
$update_recurring = true;
$old = $this->get_event($event);
// modify a recurring event, check submitted savemode to do the right things
if ($old['recurrence'] || $old['recurrence_id']) {
$master = $old['recurrence_id'] ? $this->get_event(array('id' => $old['recurrence_id'])) : $old;
// keep saved exceptions (not submitted by the client)
if ($old['recurrence']['EXDATE'])
$event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
switch ($event['_savemode']) {
case 'new':
$event['uid'] = $this->cal->generate_uid();
return $this->new_event($event);
case 'current':
// add exception to master event
$master['recurrence']['EXDATE'][] = $old['start'];
$update_master = true;
// just update this occurence (decouple from master)
$update_recurring = false;
$event['recurrence_id'] = 0;
$event['recurrence'] = array();
break;
case 'future':
if ($master['id'] != $event['id']) {
// set until-date on master event, then save this instance as new recurring event
$master['recurrence']['UNTIL'] = $event['start'] - 86400;
unset($master['recurrence']['COUNT']);
$update_master = true;
// if recurrence COUNT, update value to the correct number of future occurences
if ($event['recurrence']['COUNT']) {
$sqlresult = $this->rc->db->query(sprintf(
"SELECT event_id FROM " . $this->db_events . "
WHERE calendar_id IN (%s)
AND start >= %s
AND recurrence_id=?",
$this->calendar_ids,
$this->rc->db->fromunixtime($event['start'])
),
$master['id']);
if ($count = $this->rc->db->num_rows($sqlresult))
$event['recurrence']['COUNT'] = $count;
}
$update_recurring = true;
$event['recurrence_id'] = 0;
break;
}
// else: 'future' == 'all' if modifying the master event
default: // 'all' is default
$event['id'] = $master['id'];
$event['recurrence_id'] = 0;
// use start date from master but try to be smart on time or duration changes
$old_start_date = date('Y-m-d', $old['start']);
$old_start_time = date('H:i', $old['start']);
$old_duration = $old['end'] - $old['start'];
$new_start_date = date('Y-m-d', $event['start']);
$new_start_time = date('H:i', $event['start']);
$new_duration = $event['end'] - $event['start'];
$diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
// shifted or resized
if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
$event['start'] = $master['start'] + ($event['start'] - $old['start']);
$event['end'] = $event['start'] + $new_duration;
}
break;
}
}
$success = $this->_update_event($event, $update_recurring);
if ($success && $update_master)
$this->_update_event($master, true);
return $success;
}
return false;
}
/**
* Convert save data to be used in SQL statements
*/
private function _save_preprocess($event)
{
// compose vcalendar-style recurrencue rule from structured data
$rrule = $event['recurrence'] ? calendar::to_rrule($event['recurrence']) : '';
$event['_recurrence'] = rtrim($rrule, ';');
$event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]);
if (isset($event['allday'])) {
$event['all_day'] = $event['allday'] ? 1 : 0;
}
// compute absolute time to notify the user
$event['notifyat'] = $this->_get_notification($event);
// process event attendees
$_attendees = '';
foreach ((array)$event['attendees'] as $attendee) {
if (!$attendee['name'] && !$attendee['email'])
continue;
$_attendees .= 'NAME="'.addcslashes($attendee['name'], '"') . '"' .
';STATUS=' . $attendee['status'].
';ROLE=' . $attendee['role'] .
';EMAIL=' . $attendee['email'] .
"\n";
}
$event['attendees'] = rtrim($_attendees);
return $event;
}
/**
* Compute absolute time to notify the user
*/
private function _get_notification($event)
{
if ($event['alarms']) {
list($trigger, $action) = explode(':', $event['alarms']);
$notify = calendar::parse_alaram_value($trigger);
if (!empty($notify[1])){ // offset
$mult = 1;
switch ($notify[1]) {
case '-M': $mult = -60; break;
case '+M': $mult = 60; break;
case '-H': $mult = -3600; break;
case '+H': $mult = 3600; break;
case '-D': $mult = -86400; break;
case '+D': $mult = 86400; break;
}
$offset = $notify[0] * $mult;
$refdate = $mult > 0 ? $event['end'] : $event['start'];
$notify_at = $refdate + $offset;
}
else { // absolute timestamp
$notify_at = $notify[0];
}
if ($event['start'] > time())
return date('Y-m-d H:i:s', $notify_at);
}
return null;
}
/**
* Save the given event record to database
*
* @param array Event data, already passed through self::_save_preprocess()
* @param boolean True if recurring events instances should be updated, too
*/
private function _update_event($event, $update_recurring = true)
{
$event = $this->_save_preprocess($event);
$sql_set = array();
$set_cols = array('all_day', 'recurrence_id', 'title', 'description', 'location', 'categories', 'free_busy', 'priority', 'sensitivity', 'attendees', 'alarms', 'notifyat');
foreach ($set_cols as $col) {
if (isset($event[$col]))
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]);
}
if ($event['_recurrence'])
$sql_set[] = $this->rc->db->quote_identifier('recurrence') . '=' . $this->rc->db->quote($event['_recurrence']);
if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar'])
$sql_set[] = 'calendar_id=' . $this->rc->db->quote($event['calendar']);
$query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_events . "
SET changed=%s, start=%s, end=%s %s
WHERE event_id=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$this->rc->db->now(),
$this->rc->db->fromunixtime($event['start']),
$this->rc->db->fromunixtime($event['end']),
($sql_set ? ', ' . join(', ', $sql_set) : '')
),
$event['id']
);
$success = $this->rc->db->affected_rows($query);
// add attachments
if ($success && !empty($event['attachments'])) {
foreach ($event['attachments'] as $attachment) {
$this->add_attachment($attachment, $event['id']);
unset($attachment);
}
}
// remove attachments
if ($success && !empty($event['deleted_attachments'])) {
foreach ($event['deleted_attachments'] as $attachment) {
$this->remove_attachment($attachment, $event['id']);
}
}
if ($success) {
unset($this->cache[$event['id']]);
if ($update_recurring)
$this->_update_recurring($event);
}
return $success;
}
/**
* Insert "fake" entries for recurring occurences of this event
*/
private function _update_recurring($event)
{
if (empty($this->calendars))
return;
// clear existing recurrence copies
$this->rc->db->query(
"DELETE FROM " . $this->db_events . "
WHERE recurrence_id=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$event['id']
);
// create new fake entries
if ($event['recurrence']) {
// include library class
require_once($this->cal->home . '/lib/calendar_recurrence.php');
$recurrence = new calendar_recurrence($this->cal, $event);
$duration = $event['end'] - $event['start'];
while ($next_ts = $recurrence->next_start()) {
$notify_at = $this->_get_notification(array('alarms' => $event['alarms'], 'start' => $next_ts, 'end' => $next_ts + $duration));
$query = $this->rc->db->query(sprintf(
"INSERT INTO " . $this->db_events . "
(calendar_id, recurrence_id, created, changed, uid, start, end, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, alarms, notifyat)
SELECT calendar_id, ?, %s, %s, uid, %s, %s, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, alarms, ?
FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")",
$this->rc->db->now(),
$this->rc->db->now(),
$this->rc->db->fromunixtime($next_ts),
$this->rc->db->fromunixtime($next_ts + $duration)
),
$event['id'],
$notify_at,
$event['id']
);
if (!$this->rc->db->affected_rows($query))
break;
// stop adding events for inifinite recurrence after 20 years
if (++$count > 999 || (!$recurrence->recurEnd && !$recurrence->recurCount && $next->year > date('Y') + 20))
break;
}
}
}
/**
* Move a single event
*
* @param array Hash array with event properties
* @see calendar_driver::move_event()
*/
public function move_event($event)
{
// let edit_event() do all the magic
return $this->edit_event($event + (array)$this->get_event($event));
}
/**
* Resize a single event
*
* @param array Hash array with event properties
* @see calendar_driver::resize_event()
*/
public function resize_event($event)
{
// let edit_event() do all the magic
return $this->edit_event($event + (array)$this->get_event($event));
}
/**
* Remove a single event from the database
*
* @param array Hash array with event properties
* @param boolean Remove record irreversible (@TODO)
*
* @see calendar_driver::remove_event()
*/
public function remove_event($event, $force = true)
{
if (!empty($this->calendars)) {
$event += (array)$this->get_event($event);
$master = $event;
$update_master = false;
$savemode = 'all';
// read master if deleting a recurring event
if ($event['recurrence'] || $event['recurrence_id']) {
$master = $event['recurrence_id'] ? $this->get_event(array('id' => $old['recurrence_id'])) : $event;
$savemode = $event['_savemode'];
}
switch ($savemode) {
case 'current':
// add exception to master event
$master['recurrence']['EXDATE'][] = $event['start'];
$update_master = true;
// just delete this single occurence
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_events . "
WHERE calendar_id IN (" . $this->calendar_ids . ")
AND event_id=?",
$event['id']
);
break;
case 'future':
if ($master['id'] != $event['id']) {
// set until-date on master event
$master['recurrence']['UNTIL'] = $event['start'] - 86400;
unset($master['recurrence']['COUNT']);
$update_master = true;
// delete this and all future instances
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_events . "
WHERE calendar_id IN (" . $this->calendar_ids . ")
AND start >= " . $this->rc->db->fromunixtime($old['start']) . "
AND recurrence_id=?",
$master['id']
);
break;
}
// else: future == all if modifying the master event
default: // 'all' is default
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_events . "
WHERE (event_id=? OR recurrence_id=?)
AND calendar_id IN (" . $this->calendar_ids . ")",
$master['id'],
$master['id']
);
break;
}
$success = $this->rc->db->affected_rows($query);
if ($success && $update_master)
$this->_update_event($master, true);
return $success;
}
return false;
}
/**
* Return data of a specific event
* @param mixed Hash array with event properties or event UID
* @param boolean Only search in writeable calendars (currently ignored)
* @return array Hash array with event properties
*/
public function get_event($event, $writeable = null)
{
$id = is_array($event) ? ($event['id'] ? $event['id'] : $event['uid']) : $event;
$col = $event['id'] && is_numeric($event['id']) ? 'event_id' : 'uid';
if ($this->cache[$id])
return $this->cache[$id];
$result = $this->rc->db->query(sprintf(
"SELECT e.*, COUNT(a.attachment_id) AS _attachments FROM " . $this->db_events . " AS e
LEFT JOIN " . $this->db_attachments . " AS a ON (a.event_id = e.event_id OR a.event_id = e.recurrence_id)
WHERE e.calendar_id IN (%s)
AND e.$col=?",
$this->calendar_ids
),
$id);
if ($result && ($event = $this->rc->db->fetch_assoc($result)) && $event['event_id']) {
$this->cache[$id] = $this->_read_postprocess($event);
return $this->cache[$id];
}
return false;
}
/**
* Get event data
*
* @see calendar_driver::load_events()
*/
public function load_events($start, $end, $query = null, $calendars = null)
{
if (empty($calendars))
$calendars = array_keys($this->calendars);
else if (is_string($calendars))
$calendars = explode(',', $calendars);
// only allow to select from calendars of this use
$calendar_ids = array_map(array($this->rc->db, 'quote'), array_intersect($calendars, array_keys($this->calendars)));
// compose (slow) SQL query for searching
// FIXME: improve searching using a dedicated col and normalized values
if ($query) {
foreach (array('title','location','description','categories','attendees') as $col)
$sql_query[] = $this->rc->db->ilike($col, '%'.$query.'%');
$sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
}
$events = array();
if (!empty($calendar_ids)) {
$result = $this->rc->db->query(sprintf(
"SELECT e.*, COUNT(a.attachment_id) AS _attachments FROM " . $this->db_events . " AS e
LEFT JOIN " . $this->db_attachments . " AS a ON (a.event_id = e.event_id OR a.event_id = e.recurrence_id)
WHERE e.calendar_id IN (%s)
AND e.start <= %s AND e.end >= %s
%s
GROUP BY e.event_id",
join(',', $calendar_ids),
$this->rc->db->fromunixtime($end),
$this->rc->db->fromunixtime($start),
$sql_add
));
while ($result && ($event = $this->rc->db->fetch_assoc($result))) {
$events[] = $this->_read_postprocess($event);
}
}
return $events;
}
/**
* Convert sql record into a rcube style event object
*/
private function _read_postprocess($event)
{
$free_busy_map = array_flip($this->free_busy_map);
$event['id'] = $event['event_id'];
$event['start'] = strtotime($event['start']);
$event['end'] = strtotime($event['end']);
$event['allday'] = intval($event['all_day']);
$event['changed'] = strtotime($event['changed']);
$event['free_busy'] = $free_busy_map[$event['free_busy']];
$event['calendar'] = $event['calendar_id'];
$event['recurrence_id'] = intval($event['recurrence_id']);
// parse recurrence rule
if ($event['recurrence'] && preg_match_all('/([A-Z]+)=([^;]+);?/', $event['recurrence'], $m, PREG_SET_ORDER)) {
$event['recurrence'] = array();
foreach ($m as $rr) {
if (is_numeric($rr[2]))
$rr[2] = intval($rr[2]);
else if ($rr[1] == 'UNTIL')
$rr[2] = strtotime($rr[2]);
else if ($rr[1] == 'EXDATE')
$rr[2] = array_map('strtotime', explode(',', $rr[2]));
$event['recurrence'][$rr[1]] = $rr[2];
}
}
if ($event['_attachments'] > 0)
$event['attachments'] = (array)$this->list_attachments($event);
// decode serialized event attendees
if ($event['attendees']) {
$attendees = array();
foreach (explode("\n", $event['attendees']) as $line) {
$att = array();
foreach (rcube_explode_quoted_string(';', $line) as $prop) {
list($key, $value) = explode("=", $prop);
$att[strtolower($key)] = stripslashes(trim($value, '""'));
}
$attendees[] = $att;
}
$event['attendees'] = $attendees;
}
unset($event['event_id'], $event['calendar_id'], $event['notifyat'], $event['all_day'], $event['_attachments']);
return $event;
}
/**
* Get a list of pending alarms to be displayed to the user
*
* @see calendar_driver::pending_alarms()
*/
public function pending_alarms($time, $calendars = null)
{
if (empty($calendars))
$calendars = array_keys($this->calendars);
else if (is_string($calendars))
$calendars = explode(',', $calendars);
// only allow to select from calendars with activated alarms
$calendar_ids = array();
foreach ($calendars as $cid) {
if ($this->calendars[$cid] && $this->calendars[$cid]['showalarms'])
$calendar_ids[] = $cid;
}
$calendar_ids = array_map(array($this->rc->db, 'quote'), $calendar_ids);
$alarms = array();
if (!empty($calendar_ids)) {
$result = $this->rc->db->query(sprintf(
"SELECT * FROM " . $this->db_events . "
WHERE calendar_id IN (%s)
AND notifyat <= %s AND end > %s",
join(',', $calendar_ids),
$this->rc->db->fromunixtime($time),
$this->rc->db->fromunixtime($time)
));
while ($result && ($event = $this->rc->db->fetch_assoc($result)))
$alarms[] = $this->_read_postprocess($event);
}
return $alarms;
}
/**
* Feedback after showing/sending an alarm notification
*
* @see calendar_driver::dismiss_alarm()
*/
public function dismiss_alarm($event_id, $snooze = 0)
{
// set new notifyat time or unset if not snoozed
$notify_at = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
$query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_events . "
SET changed=%s, notifyat=?
WHERE event_id=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$this->rc->db->now()),
$notify_at,
$event_id
);
return $this->rc->db->affected_rows($query);
}
/**
* Save an attachment related to the given event
*/
private function add_attachment($attachment, $event_id)
{
$data = $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']);
$query = $this->rc->db->query(
"INSERT INTO " . $this->db_attachments .
" (event_id, filename, mimetype, size, data)" .
" VALUES (?, ?, ?, ?, ?)",
$event_id,
$attachment['name'],
$attachment['mimetype'],
strlen($data),
base64_encode($data)
);
return $this->rc->db->affected_rows($query);
}
/**
* Remove a specific attachment from the given event
*/
private function remove_attachment($attachment_id, $event_id)
{
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_attachments .
" WHERE attachment_id = ?" .
" AND event_id IN (SELECT event_id FROM " . $this->db_events .
" WHERE event_id = ?" .
" AND calendar_id IN (" . $this->calendar_ids . "))",
$attachment_id,
$event_id
);
return $this->rc->db->affected_rows($query);
}
/**
* List attachments of specified event
*/
public function list_attachments($event)
{
$attachments = array();
$event_id = $event['recurrence_id'] ? $event['recurrence_id'] : $event['event_id'];
if (!empty($this->calendar_ids)) {
$result = $this->rc->db->query(
"SELECT attachment_id AS id, filename AS name, mimetype, size " .
" FROM " . $this->db_attachments .
" WHERE event_id IN (SELECT event_id FROM " . $this->db_events .
" WHERE event_id=?" .
" AND calendar_id IN (" . $this->calendar_ids . "))".
" ORDER BY filename",
$event['recurrence_id'] ? $event['recurrence_id'] : $event['event_id']
);
while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
$attachments[] = $arr;
}
}
return $attachments;
}
/**
* Get attachment properties
*/
public function get_attachment($id, $event)
{
if (!empty($this->calendar_ids)) {
$result = $this->rc->db->query(
"SELECT attachment_id AS id, filename AS name, mimetype, size " .
" FROM " . $this->db_attachments .
" WHERE attachment_id=?".
" AND event_id=?",
$id,
$event['recurrence_id'] ? $event['recurrence_id'] : $event['id']
);
if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
return $arr;
}
}
return null;
}
/**
* Get attachment body
*/
public function get_attachment_body($id, $event)
{
if (!empty($this->calendar_ids)) {
$result = $this->rc->db->query(
"SELECT data " .
" FROM " . $this->db_attachments .
" WHERE attachment_id=?".
" AND event_id=?",
$id,
$event['id']
);
if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
return base64_decode($arr['data']);
}
}
return null;
}
/**
* Remove the given category
*/
public function remove_category($name)
{
$query = $this->rc->db->query(
"UPDATE " . $this->db_events . "
SET categories=''
WHERE categories=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$name
);
return $this->rc->db->affected_rows($query);
}
/**
* Update/replace a category
*/
public function replace_category($oldname, $name, $color)
{
$query = $this->rc->db->query(
"UPDATE " . $this->db_events . "
SET categories=?
WHERE categories=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$name,
$oldname
);
return $this->rc->db->affected_rows($query);
}
}
diff --git a/plugins/calendar/drivers/database/sql/mysql.sql b/plugins/calendar/drivers/database/sql/mysql.sql
index ea3ff028..8d6f1736 100644
--- a/plugins/calendar/drivers/database/sql/mysql.sql
+++ b/plugins/calendar/drivers/database/sql/mysql.sql
@@ -1,77 +1,77 @@
/**
* Roundcube Calendar
*
* Plugin to add a calendar to Roundcube.
*
* @version 0.3 beta
* @author Lazlo Westerhof
* @author Thomas Bruederli
* @url http://rc-calendar.lazlo.me
- * @licence GNU GPL
+ * @licence GNU AGPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
CREATE TABLE `calendars` (
`calendar_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
`name` varchar(255) NOT NULL,
`color` varchar(8) NOT NULL,
`showalarms` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY(`calendar_id`),
CONSTRAINT `fk_calendars_user_id` FOREIGN KEY (`user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
CREATE TABLE `events` (
`event_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`calendar_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
`recurrence_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
`uid` varchar(255) NOT NULL DEFAULT '',
`created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`start` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`end` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`recurrence` varchar(255) DEFAULT NULL,
`title` varchar(255) NOT NULL,
`description` text NOT NULL,
`location` varchar(255) NOT NULL DEFAULT '',
`categories` varchar(255) NOT NULL DEFAULT '',
`all_day` tinyint(1) NOT NULL DEFAULT '0',
`free_busy` tinyint(1) NOT NULL DEFAULT '0',
`priority` tinyint(1) NOT NULL DEFAULT '0',
`sensitivity` tinyint(1) NOT NULL DEFAULT '0',
`alarms` varchar(255) DEFAULT NULL,
`attendees` text DEFAULT NULL,
`notifyat` datetime DEFAULT NULL,
PRIMARY KEY(`event_id`),
INDEX `uid_idx` (`uid`,`calendar_id`),
INDEX `recurrence_idx` (`recurrence_id`),
CONSTRAINT `fk_events_calendar_id` FOREIGN KEY (`calendar_id`)
REFERENCES `calendars`(`calendar_id`) ON DELETE CASCADE ON UPDATE CASCADE
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
CREATE TABLE `attachments` (
`attachment_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`event_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
`filename` varchar(255) NOT NULL DEFAULT '',
`mimetype` varchar(255) NOT NULL DEFAULT '',
`size` int(11) NOT NULL DEFAULT '0',
`data` longtext NOT NULL DEFAULT '',
PRIMARY KEY(`attachment_id`),
CONSTRAINT `fk_attachments_event_id` FOREIGN KEY (`event_id`)
REFERENCES `events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
CREATE TABLE `itipinvitations` (
`token` VARCHAR(64) NOT NULL,
`event_uid` VARCHAR(255) NOT NULL,
`user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
`event` TEXT NOT NULL,
`expires` DATETIME DEFAULT NULL,
`cancelled` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY(`token`),
INDEX `uid_idx` (`event_uid`,`user_id`),
CONSTRAINT `fk_itipinvitations_user_id` FOREIGN KEY (`user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
diff --git a/plugins/calendar/drivers/database/sql/postgresql.sql b/plugins/calendar/drivers/database/sql/postgresql.sql
index 25358041..f9896a49 100644
--- a/plugins/calendar/drivers/database/sql/postgresql.sql
+++ b/plugins/calendar/drivers/database/sql/postgresql.sql
@@ -1,81 +1,81 @@
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.3 beta
* @author Lazlo Westerhof
* @author Albert Lee
* @author Aleksander Machniak <machniak@kolabsys.com>
* @url http://rc-calendar.lazlo.me
- * @licence GNU GPL
+ * @licence GNU AGPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
CREATE SEQUENCE calendar_ids
INCREMENT BY 1
NO MAXVALUE
NO MINVALUE
CACHE 1;
CREATE TABLE calendars (
calendar_id integer DEFAULT nextval('calendar_ids'::regclass) NOT NULL,
user_id integer NOT NULL
REFERENCES users (user_id) ON UPDATE CASCADE ON DELETE CASCADE,
name varchar(255) NOT NULL,
color varchar(8) NOT NULL,
showalarms smallint NOT NULL DEFAULT 1,
PRIMARY KEY (calendar_id)
);
CREATE SEQUENCE event_ids
INCREMENT BY 1
NO MAXVALUE
NO MINVALUE
CACHE 1;
CREATE TABLE events (
event_id integer DEFAULT nextval('event_ids'::regclass) NOT NULL,
calendar_id integer NOT NULL
REFERENCES calendars (calendar_id) ON UPDATE CASCADE ON DELETE CASCADE,
recurence_id integer NOT NULL DEFAULT 0,
uid varchar(255) NOT NULL DEFAULT '',
created timestamp without time zone DEFAULT now() NOT NULL,
changed timestamp without time zone DEFAULT now(),
"start" timestamp without time zone DEFAULT now() NOT NULL,
"end" timestamp without time zone DEFAULT now() NOT NULL,
recurrence varchar(255) DEFAULT NULL,
title character varying(255) NOT NULL,
description text NOT NULL,
location character varying(255) NOT NULL,
categories character varying(255) NOT NULL,
all_day smallint NOT NULL DEFAULT 0,
free_busy smallint NOT NULL DEFAULT 0,
priority smallint NOT NULL DEFAULT 0,
sensitivity smallint NOT NULL DEFAULT 0,
alarms varchar(255) DEFAULT NULL,
attendees text DEFAULT NULL,
notifyat timestamp without time zone DEFAULT NULL
PRIMARY KEY (event_id)
);
CREATE SEQUENCE attachment_ids
INCREMENT BY 1
NO MAXVALUE
NO MINVALUE
CACHE 1;
CREATE TABLE attachments (
attachment_id integer DEFAULT nextval('attachment_ids'::regclass) NOT NULL,
event_id integer NOT NULL
REFERENCES events (event_id) ON DELETE CASCADE ON UPDATE CASCADE,
filename varchar(255) NOT NULL DEFAULT '',
mimetype varchar(255) NOT NULL DEFAULT '',
size integer NOT NULL DEFAULT 0,
data text NOT NULL DEFAULT '',
PRIMARY KEY (attachment_id)
);
diff --git a/plugins/calendar/drivers/database/sql/sqlite.sql b/plugins/calendar/drivers/database/sql/sqlite.sql
index a9fbccda..9e9e0df8 100644
--- a/plugins/calendar/drivers/database/sql/sqlite.sql
+++ b/plugins/calendar/drivers/database/sql/sqlite.sql
@@ -1,74 +1,74 @@
/**
* Roundcube Calendar
*
* Plugin to add a calendar to Roundcube.
*
* @version 0.3 beta
* @author Lazlo Westerhof
* @author Thomas Bruederli
* @author Albert Lee
* @url http://rc-calendar.lazlo.me
- * @licence GNU GPL
+ * @licence GNU AGPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
CREATE TABLE calendars (
calendar_id integer NOT NULL PRIMARY KEY,
user_id integer NOT NULL default '0',
name varchar(255) NOT NULL default '',
color varchar(255) NOT NULL default '',
showalarms tinyint(1) NOT NULL default '1',
CONSTRAINT fk_calendars_user_id FOREIGN KEY (user_id)
REFERENCES users(user_id)
);
CREATE TABLE events (
event_id integer NOT NULL PRIMARY KEY,
calendar_id integer NOT NULL default '0',
recurrence_id integer NOT NULL default '0',
uid varchar(255) NOT NULL default '',
created datetime NOT NULL default '1000-01-01 00:00:00',
changed datetime NOT NULL default '1000-01-01 00:00:00',
start datetime NOT NULL default '1000-01-01 00:00:00',
end datetime NOT NULL default '1000-01-01 00:00:00',
recurrence varchar(255) default NULL,
title varchar(255) NOT NULL,
description text NOT NULL,
location varchar(255) NOT NULL default '',
categories varchar(255) NOT NULL default '',
all_day tinyint(1) NOT NULL default '0',
free_busy tinyint(1) NOT NULL default '0',
priority tinyint(1) NOT NULL default '0',
sensitivity tinyint(1) NOT NULL default '0',
alarms varchar(255) default NULL,
attendees text default NULL,
notifyat datetime default NULL,
CONSTRAINT fk_events_calendar_id FOREIGN KEY (calendar_id)
REFERENCES calendars(calendar_id)
);
CREATE TABLE attachments (
attachment_id integer NOT NULL PRIMARY KEY,
event_id integer NOT NULL default '0',
filename varchar(255) NOT NULL default '',
mimetype varchar(255) NOT NULL default '',
size integer NOT NULL default '0',
data text NOT NULL default '',
CONSTRAINT fk_attachment_event_id FOREIGN KEY (event_id)
REFERENCES events(event_id)
);
CREATE TABLE itipinvitations (
token varchar(64) NOT NULL PRIMARY KEY,
event_uid varchar(255) NOT NULL,
user_id integer NOT NULL default '0',
event text NOT NULL,
expires datetime NOT NULL default '1000-01-01 00:00:00',
cancelled tinyint(1) NOT NULL default '0',
CONSTRAINT fk_itipinvitations_user_id FOREIGN KEY (user_id)
REFERENCES users(user_id)
);
CREATE INDEX ix_itipinvitations_uid ON itipinvitations(event_uid,user_id);
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 7569f59f..008b42f1 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -1,779 +1,779 @@
<?php
/**
* Kolab calendar storage class
*
* @version 0.7-beta
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_calendar
{
public $id;
public $ready = false;
public $readonly = true;
public $attachments = true;
public $alarms = false;
public $categories = array();
public $storage;
private $cal;
private $events;
private $id2uid;
private $imap_folder = 'INBOX/Calendar';
private $namespace;
private $search_fields = array('title', 'description', 'location', '_attendees');
private $sensitivity_map = array('public', 'private', 'confidential');
private $priority_map = array('low' => 9, 'normal' => 5, 'high' => 1);
private $role_map = array('REQ-PARTICIPANT' => 'required', 'OPT-PARTICIPANT' => 'optional', 'CHAIR' => 'resource');
private $status_map = array('NEEDS-ACTION' => 'none', 'TENTATIVE' => 'tentative', 'CONFIRMED' => 'accepted', 'ACCEPTED' => 'accepted', 'DECLINED' => 'declined');
private $month_map = array('', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december');
private $weekday_map = array('MO'=>'monday', 'TU'=>'tuesday', 'WE'=>'wednesday', 'TH'=>'thursday', 'FR'=>'friday', 'SA'=>'saturday', 'SU'=>'sunday');
/**
* Default constructor
*/
public function __construct($imap_folder, $calendar)
{
$this->cal = $calendar;
if (strlen($imap_folder))
$this->imap_folder = $imap_folder;
// ID is derrived from folder name
$this->id = rcube_kolab::folder_id($this->imap_folder);
// fetch objects from the given IMAP folder
$this->storage = rcube_kolab::get_storage($this->imap_folder);
$this->ready = !PEAR::isError($this->storage);
// Set readonly and alarms flags according to folder permissions
if ($this->ready) {
if ($this->get_owner() == $_SESSION['username']) {
$this->readonly = false;
$this->alarms = true;
}
else {
$rights = $this->storage->_folder->getMyRights();
if (!PEAR::isError($rights)) {
if (strpos($rights, 'i') !== false)
$this->readonly = false;
}
}
// user-specific alarms settings win
$prefs = $this->cal->rc->config->get('kolab_calendars', array());
if (isset($prefs[$this->id]['showalarms']))
$this->alarms = $prefs[$this->id]['showalarms'];
}
}
/**
* Getter for a nice and human readable name for this calendar
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @return string Name of this calendar
*/
public function get_name()
{
$folder = rcube_kolab::object_name($this->imap_folder, $this->namespace);
return $folder;
}
/**
* Getter for the IMAP folder name
*
* @return string Name of the IMAP folder
*/
public function get_realname()
{
return $this->imap_folder;
}
/**
* Getter for the IMAP folder owner
*
* @return string Name of the folder owner
*/
public function get_owner()
{
return $this->storage->_folder->getOwner();
}
/**
* Getter for the name of the namespace to which the IMAP folder belongs
*
* @return string Name of the namespace (personal, other, shared)
*/
public function get_namespace()
{
if ($this->namespace === null) {
$this->namespace = rcube_kolab::folder_namespace($this->imap_folder);
}
return $this->namespace;
}
/**
* Getter for the top-end calendar folder name (not the entire path)
*
* @return string Name of this calendar
*/
public function get_foldername()
{
$parts = explode('/', $this->imap_folder);
return rcube_charset_convert(end($parts), 'UTF7-IMAP');
}
/**
* Return color to display this calendar
*/
public function get_color()
{
// color is defined in folder METADATA
if ($color = $this->storage->_folder->getKolabAttribute('color', HORDE_ANNOT_READ_PRIVATE_SHARED)) {
return $color;
}
// calendar color is stored in user prefs (temporary solution)
$prefs = $this->cal->rc->config->get('kolab_calendars', array());
if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
return $prefs[$this->id]['color'];
return 'cc0000';
}
/**
* Return the corresponding Kolab_Folder instance
*/
public function get_folder()
{
return $this->storage->_folder;
}
/**
* Getter for the attachment body
*/
public function get_attachment_body($id)
{
return $this->storage->getAttachment($id);
}
/**
* Getter for a single event object
*/
public function get_event($id)
{
$this->_fetch_events();
// event not found, maybe a recurring instance is requested
if (!$this->events[$id]) {
$master_id = preg_replace('/-\d+$/', '', $id);
if ($this->events[$master_id] && $this->events[$master_id]['recurrence']) {
$master = $this->events[$master_id];
$this->_get_recurring_events($master, $master['start'], $master['start'] + 86400 * 365 * 10, $id);
}
}
return $this->events[$id];
}
/**
* @param integer Event's new start (unix timestamp)
* @param integer Event's new end (unix timestamp)
* @param string Search query (optional)
* @param boolean Strip virtual events (optional)
* @return array A list of event records
*/
public function list_events($start, $end, $search = null, $virtual = 1)
{
$this->_fetch_events();
if (!empty($search))
$search = mb_strtolower($search);
$events = array();
foreach ($this->events as $id => $event) {
// remember seen categories
if ($event['categories'])
$this->categories[$event['categories']]++;
// filter events by search query
if (!empty($search)) {
$hit = false;
foreach ($this->search_fields as $col) {
$sval = is_array($col) ? $event[$col[0]][$col[1]] : $event[$col];
if (empty($sval))
continue;
// do a simple substring matching (to be improved)
$val = mb_strtolower($sval);
if (strpos($val, $search) !== false) {
$hit = true;
break;
}
}
if (!$hit) // skip this event if not match with search term
continue;
}
// list events in requested time window
if ($event['start'] <= $end && $event['end'] >= $start) {
unset($event['_attendees']);
$events[] = $event;
}
// resolve recurring events
if ($event['recurrence'] && $virtual == 1) {
unset($event['_attendees']);
$events = array_merge($events, $this->_get_recurring_events($event, $start, $end));
}
}
return $events;
}
/**
* Create a new event record
*
* @see calendar_driver::new_event()
*
* @return mixed The created record ID on success, False on error
*/
public function insert_event($event)
{
if (!is_array($event))
return false;
//generate new event from RC input
$object = $this->_from_rcube_event($event);
$saved = $this->storage->save($object);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving event object to Kolab server:" . $saved->getMessage()),
true, false);
$saved = false;
}
else {
$event['id'] = $event['uid'];
$this->events[$event['uid']] = $event;
}
return $saved;
}
/**
* Update a specific event record
*
* @see calendar_driver::new_event()
* @return boolean True on success, False on error
*/
public function update_event($event)
{
$updated = false;
$old = $this->storage->getObject($event['id']);
if (PEAR::isError($old))
return false;
$old['recurrence'] = ''; # clear old field, could have been removed in new, too
$object = array_merge($old, $this->_from_rcube_event($event));
$saved = $this->storage->save($object, $event['id']);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving event object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
$updated = true;
$this->events[$event['id']] = $this->_to_rcube_event($object);
}
return $updated;
}
/**
* Delete an event record
*
* @see calendar_driver::remove_event()
* @return boolean True on success, False on error
*/
public function delete_event($event, $force = true)
{
$deleted = false;
if (!$force) {
// Get IMAP object ID
$imap_uid = $this->storage->_getStorageId($event['id']);
}
$deleteme = $this->storage->delete($event['id'], $force);
if (PEAR::isError($deleteme)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting event object from Kolab server:" . $deleteme->getMessage()),
true, false);
}
else {
// Save IMAP object ID in session, will be used for restore action
if ($imap_uid)
$_SESSION['kolab_delete_uids'][$event['id']] = $imap_uid;
$deleted = true;
}
return $deleted;
}
/**
* Restore deleted event record
*
* @see calendar_driver::undelete_event()
* @return boolean True on success, False on error
*/
public function restore_event($event)
{
$imap_uid = $_SESSION['kolab_delete_uids'][$event['id']];
if (!$imap_uid)
return false;
$session = &Horde_Kolab_Session::singleton();
$imap = &$session->getImap();
if (is_object($imap) && is_a($imap, 'PEAR_Error')) {
$error = $imap;
}
else {
$result = $imap->select($this->imap_folder);
if (is_object($result) && is_a($result, 'PEAR_Error')) {
$error = $result;
}
else {
$result = $imap->undeleteMessages($imap_uid);
if (is_object($result) && is_a($result, 'PEAR_Error')) {
$error = $result;
}
else {
// re-sync the cache
$this->storage->synchronize();
}
}
}
if ($error) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error undeleting an event object(s) from the Kolab server:" . $error->getMessage()),
true, false);
return false;
}
$rcmail = rcmail::get_instance();
$rcmail->session->remove('kolab_delete_uids');
return true;
}
/**
* Simply fetch all records and store them in private member vars
* We thereby rely on cahcing done by the Horde classes
*/
private function _fetch_events()
{
if (!isset($this->events)) {
$this->events = array();
foreach ((array)$this->storage->getObjects() as $record) {
$event = $this->_to_rcube_event($record);
$this->events[$event['id']] = $event;
}
}
}
/**
* Create instances of a recurring event
*/
public function _get_recurring_events($event, $start, $end, $event_id = null)
{
// include library class
require_once($this->cal->home . '/lib/calendar_recurrence.php');
$recurrence = new calendar_recurrence($this->cal, $event);
$events = array();
$duration = $event['end'] - $event['start'];
$i = 0;
while ($rec_start = $recurrence->next_start()) {
$rec_end = $rec_start + $duration;
$rec_id = $event['id'] . '-' . ++$i;
// add to output if in range
if (($rec_start <= $end && $rec_end >= $start) || ($event_id && $rec_id == $event_id)) {
$rec_event = $event;
$rec_event['id'] = $rec_id;
$rec_event['recurrence_id'] = $event['id'];
$rec_event['start'] = $rec_start;
$rec_event['end'] = $rec_end;
$rec_event['_instance'] = $i;
$events[] = $rec_event;
if ($rec_id == $event_id) {
$this->events[$rec_id] = $rec_event;
break;
}
}
else if ($rec_start > $end) // stop loop if out of range
break;
}
return $events;
}
/**
* Convert from Kolab_Format to internal representation
*/
private function _to_rcube_event($rec)
{
$start_time = date('H:i:s', $rec['start-date']);
$allday = $rec['_is_all_day'] || ($start_time == '00:00:00' && $start_time == date('H:i:s', $rec['end-date']));
if ($allday) { // in Roundcube all-day events only go from 12:00 to 13:00
$rec['start-date'] += 12 * 3600;
$rec['end-date'] -= 11 * 3600;
$rec['end-date'] -= $this->cal->gmt_offset - date('Z', $rec['end-date']); // shift times from server's timezone to user's timezone
$rec['start-date'] -= $this->cal->gmt_offset - date('Z', $rec['start-date']); // because generated with mktime() in Horde_Kolab_Format_Date::decodeDate()
// sanity check
if ($rec['end-date'] <= $rec['start-date'])
$rec['end-date'] += 86400;
}
// convert alarm time into internal format
if ($rec['alarm']) {
$alarm_value = $rec['alarm'];
$alarm_unit = 'M';
if ($rec['alarm'] % 1440 == 0) {
$alarm_value /= 1440;
$alarm_unit = 'D';
}
else if ($rec['alarm'] % 60 == 0) {
$alarm_value /= 60;
$alarm_unit = 'H';
}
$alarm_value *= -1;
}
// convert recurrence rules into internal pseudo-vcalendar format
if ($recurrence = $rec['recurrence']) {
$rrule = array(
'FREQ' => strtoupper($recurrence['cycle']),
'INTERVAL' => intval($recurrence['interval']),
);
if ($recurrence['range-type'] == 'number')
$rrule['COUNT'] = intval($recurrence['range']);
else if ($recurrence['range-type'] == 'date')
$rrule['UNTIL'] = $recurrence['range'];
if ($recurrence['day']) {
$byday = array();
$prefix = ($rrule['FREQ'] == 'MONTHLY' || $rrule['FREQ'] == 'YEARLY') ? intval($recurrence['daynumber'] ? $recurrence['daynumber'] : 1) : '';
foreach ($recurrence['day'] as $day)
$byday[] = $prefix . substr(strtoupper($day), 0, 2);
$rrule['BYDAY'] = join(',', $byday);
}
if ($recurrence['daynumber']) {
if ($recurrence['type'] == 'monthday' || $recurrence['type'] == 'daynumber')
$rrule['BYMONTHDAY'] = $recurrence['daynumber'];
else if ($recurrence['type'] == 'yearday')
$rrule['BYYEARDAY'] = $recurrence['daynumber'];
}
if ($recurrence['month']) {
$monthmap = array_flip($this->month_map);
$rrule['BYMONTH'] = strtolower($monthmap[$recurrence['month']]);
}
if ($recurrence['exclusion']) {
foreach ((array)$recurrence['exclusion'] as $excl)
$rrule['EXDATE'][] = strtotime($excl . date(' H:i:s', $rec['start-date'])); // use time of event start
}
}
$sensitivity_map = array_flip($this->sensitivity_map);
$status_map = array_flip($this->status_map);
$role_map = array_flip($this->role_map);
if (!empty($rec['_attachments'])) {
foreach ($rec['_attachments'] as $name => $attachment) {
// @TODO: 'type' and 'key' are the only supported (no 'size')
$attachments[] = array(
'id' => $attachment['key'],
'mimetype' => $attachment['type'],
'name' => $name,
);
}
}
if ($rec['organizer']) {
$attendees[] = array(
'role' => 'ORGANIZER',
'name' => $rec['organizer']['display-name'],
'email' => $rec['organizer']['smtp-address'],
'status' => 'ACCEPTED',
);
$_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
}
foreach ((array)$rec['attendee'] as $attendee) {
$attendees[] = array(
'role' => $role_map[$attendee['role']],
'name' => $attendee['display-name'],
'email' => $attendee['smtp-address'],
'status' => $status_map[$attendee['status']],
'rsvp' => $attendee['request-response'],
);
$_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
}
// Roundcube only supports one category assignment
$categories = explode(',', $rec['categories']);
return array(
'id' => $rec['uid'],
'uid' => $rec['uid'],
'title' => $rec['summary'],
'location' => $rec['location'],
'description' => $rec['body'],
'start' => $rec['start-date'],
'end' => $rec['end-date'],
'allday' => $allday,
'recurrence' => $rrule,
'alarms' => $alarm_value . $alarm_unit,
'_alarm' => intval($rec['alarm']),
'categories' => $categories[0],
'attachments' => $attachments,
'attendees' => $attendees,
'_attendees' => $_attendees,
'free_busy' => $rec['show-time-as'],
'priority' => is_numeric($rec['priority']) ? intval($rec['priority']) : (isset($this->priority_map[$rec['priority']]) ? $this->priority_map[$rec['priority']] : 0),
'sensitivity' => $sensitivity_map[$rec['sensitivity']],
'changed' => $rec['last-modification-date'],
'calendar' => $this->id,
);
}
/**
* Convert the given event record into a data structure that can be passed to Kolab_Storage backend for saving
* (opposite of self::_to_rcube_event())
*/
private function _from_rcube_event($event)
{
$priority_map = $this->priority_map;
$tz_offset = $this->cal->gmt_offset;
$object = array(
// kolab => roundcube
'uid' => $event['uid'],
'summary' => $event['title'],
'location' => $event['location'],
'body' => $event['description'],
'categories' => $event['categories'],
'start-date' => $event['start'],
'end-date' => $event['end'],
'sensitivity' =>$this->sensitivity_map[$event['sensitivity']],
'show-time-as' => $event['free_busy'],
'priority' => $event['priority'],
);
//handle alarms
if ($event['alarms']) {
//get the value
$alarmbase = explode(":", $event['alarms']);
//get number only
$avalue = preg_replace('/[^0-9]/', '', $alarmbase[0]);
if (preg_match("/H/",$alarmbase[0])) {
$object['alarm'] = $avalue*60;
} else if (preg_match("/D/",$alarmbase[0])) {
$object['alarm'] = $avalue*24*60;
} else {
$object['alarm'] = $avalue;
}
}
//recurr object/array
if (count($event['recurrence']) > 1) {
$ra = $event['recurrence'];
//Frequency abd interval
$object['recurrence']['cycle'] = strtolower($ra['FREQ']);
$object['recurrence']['interval'] = intval($ra['INTERVAL']);
//Range Type
if ($ra['UNTIL']) {
$object['recurrence']['range-type'] = 'date';
$object['recurrence']['range'] = $ra['UNTIL'];
}
if ($ra['COUNT']) {
$object['recurrence']['range-type'] = 'number';
$object['recurrence']['range'] = $ra['COUNT'];
}
//weekly
if ($ra['FREQ'] == 'WEEKLY') {
if ($ra['BYDAY']) {
foreach (split(",", $ra['BYDAY']) as $day)
$object['recurrence']['day'][] = $this->weekday_map[$day];
}
else {
// use weekday of start date if empty
$object['recurrence']['day'][] = strtolower(gmdate('l', $event['start'] + $tz_offset));
}
}
//monthly (temporary hack to follow current Horde logic)
if ($ra['FREQ'] == 'MONTHLY') {
if ($ra['BYDAY'] && preg_match('/(-?[1-4])([A-Z]+)/', $ra['BYDAY'], $m)) {
$object['recurrence']['daynumber'] = $m[1];
$object['recurrence']['day'] = array($this->weekday_map[$m[2]]);
$object['recurrence']['cycle'] = 'monthly';
$object['recurrence']['type'] = 'weekday';
}
else {
$object['recurrence']['daynumber'] = date('j', $event['start']);
$object['recurrence']['cycle'] = 'monthly';
$object['recurrence']['type'] = 'daynumber';
}
}
//yearly
if ($ra['FREQ'] == 'YEARLY') {
if (!$ra['BYMONTH'])
$ra['BYMONTH'] = gmdate('n', $event['start'] + $tz_offset);
$object['recurrence']['cycle'] = 'yearly';
$object['recurrence']['month'] = $this->month_map[intval($ra['BYMONTH'])];
if ($ra['BYDAY'] && preg_match('/(-?[1-4])([A-Z]+)/', $ra['BYDAY'], $m)) {
$object['recurrence']['type'] = 'weekday';
$object['recurrence']['daynumber'] = $m[1];
$object['recurrence']['day'] = array($this->weekday_map[$m[2]]);
}
else {
$object['recurrence']['type'] = 'monthday';
$object['recurrence']['daynumber'] = gmdate('j', $event['start'] + $tz_offset);
}
}
//exclusions
foreach ((array)$ra['EXDATE'] as $excl) {
$object['recurrence']['exclusion'][] = gmdate('Y-m-d', $excl + $tz_offset);
}
}
// whole day event
if ($event['allday']) {
$object['end-date'] += 12 * 3600; // end is at 13:00 => jump to the next day
$object['end-date'] += $tz_offset - date('Z'); // shift 00 times from user's timezone to server's timezone
$object['start-date'] += $tz_offset - date('Z'); // because Horde_Kolab_Format_Date::encodeDate() uses strftime()
// create timestamps at exactly 00:00. This is also needed for proper re-interpretation in _to_rcube_event() after updating an event
$object['start-date'] = mktime(0,0,0, date('n', $object['start-date']), date('j', $object['start-date']), date('Y', $object['start-date']));
$object['end-date'] = mktime(0,0,0, date('n', $object['end-date']), date('j', $object['end-date']), date('Y', $object['end-date']));
// sanity check: end date is same or smaller than start
if (date('Y-m-d', $object['end-date']) <= date('Y-m-d', $object['start-date']))
$object['end-date'] = mktime(13,0,0, date('n', $object['start-date']), date('j', $object['start-date']), date('Y', $object['start-date'])) + 86400;
$object['_is_all_day'] = 1;
}
// in Horde attachments are indexed by name
$object['_attachments'] = array();
if (!empty($event['attachments'])) {
$collisions = array();
foreach ($event['attachments'] as $idx => $attachment) {
// Roundcube ID has nothing to do with Horde ID, remove it
if ($attachment['content'])
unset($attachment['id']);
// Horde code assumes that there will be no more than
// one file with the same name: make filenames unique
$filename = $attachment['name'];
if ($collisions[$filename]++) {
$ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $filename, $m) ? $m[1] : null;
$attachment['name'] = basename($filename, $ext) . '-' . $collisions[$filename] . $ext;
}
// set type parameter
if ($attachment['mimetype'])
$attachment['type'] = $attachment['mimetype'];
$object['_attachments'][$attachment['name']] = $attachment;
unset($event['attachments'][$idx]);
}
}
// process event attendees
foreach ((array)$event['attendees'] as $attendee) {
$role = $attendee['role'];
if ($role == 'ORGANIZER') {
$object['organizer'] = array(
'display-name' => $attendee['name'],
'smtp-address' => $attendee['email'],
);
}
else {
$object['attendee'][] = array(
'display-name' => $attendee['name'],
'smtp-address' => $attendee['email'],
'status' => $this->status_map[$attendee['status']],
'role' => $this->role_map[$role],
'request-response' => $attendee['rsvp'],
);
}
}
return $object;
}
}
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 61b1b6a0..56f89bd4 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1,1228 +1,1228 @@
<?php
/**
* Kolab driver for the Calendar plugin
*
* @version 0.7-beta
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
require_once(dirname(__FILE__) . '/kolab_calendar.php');
class kolab_driver extends calendar_driver
{
// features this backend supports
public $alarms = true;
public $attendees = true;
public $freebusy = true;
public $attachments = true;
public $undelete = true;
public $categoriesimmutable = true;
private $rc;
private $cal;
private $calendars;
private $has_writeable = false;
/**
* Default constructor
*/
public function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
$this->_read_calendars();
$this->cal->register_action('push-freebusy', array($this, 'push_freebusy'));
$this->cal->register_action('calendar-acl', array($this, 'calendar_acl'));
}
/**
* Read available calendars from server
*/
private function _read_calendars()
{
// already read sources
if (isset($this->calendars))
return $this->calendars;
// get all folders that have "event" type
$folders = rcube_kolab::get_folders('event');
$this->calendars = array();
if (PEAR::isError($folders)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to list calendar folders from Kolab server:" . $folders->getMessage()),
true, false);
}
else {
// convert to UTF8 and sort
$names = array();
foreach ($folders as $folder)
$names[$folder->name] = rcube_charset_convert($folder->name, 'UTF7-IMAP');
asort($names, SORT_LOCALE_STRING);
foreach ($names as $utf7name => $name) {
$calendar = new kolab_calendar($utf7name, $this->cal);
$this->calendars[$calendar->id] = $calendar;
if (!$calendar->readonly)
$this->has_writeable = true;
}
}
return $this->calendars;
}
/**
* Get a list of available calendars from this source
*/
public function list_calendars()
{
// attempt to create a default calendar for this user
if (!$this->has_writeable) {
if ($this->create_calendar(array('name' => 'Calendar', 'color' => 'cc0000'))) {
unset($this->calendars);
$this->_read_calendars();
}
}
$calendars = $names = array();
foreach ($this->calendars as $id => $cal) {
if ($cal->ready) {
$name = $origname = $cal->get_name();
// find folder prefix to truncate (the same code as in kolab_addressbook plugin)
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i].' &raquo; ') === 0) {
$length = strlen($names[$i].' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
break;
}
}
$names[] = $origname;
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => $name,
'editname' => $cal->get_foldername(),
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
'class_name' => $cal->get_namespace(),
'active' => rcube_kolab::is_subscribed($cal->get_realname()),
);
}
}
return $calendars;
}
/**
* Create a new calendar assigned to the current user
*
* @param array Hash array with calendar properties
* name: Calendar name
* color: The color of the calendar
* @return mixed ID of the calendar on success, False on error
*/
public function create_calendar($prop)
{
$folder = $this->folder_update($prop);
if ($folder === false) {
return false;
}
// subscribe to new calendar by default
$this->rc->imap_connect();
$this->rc->imap->subscribe($folder);
// create ID
$id = rcube_kolab::folder_id($folder);
// save color in user prefs (temp. solution)
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
if (isset($prop['color']))
$prefs['kolab_calendars'][$id]['color'] = $prop['color'];
if (isset($prop['showalarms']))
$prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_calendars'][$id])
$this->rc->user->save_prefs($prefs);
return $id;
}
/**
* Update properties of an existing calendar
*
* @see calendar_driver::edit_calendar()
*/
public function edit_calendar($prop)
{
if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
$oldfolder = $cal->get_realname();
$newfolder = $this->folder_update($prop);
if ($newfolder === false) {
return false;
}
// create ID
$id = rcube_kolab::folder_id($newfolder);
// fallback to local prefs
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
unset($prefs['kolab_calendars'][$prop['id']]);
if (isset($prop['color']))
$prefs['kolab_calendars'][$id]['color'] = $prop['color'];
if (isset($prop['showalarms']))
$prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_calendars'][$id])
$this->rc->user->save_prefs($prefs);
return true;
}
return false;
}
/**
* Set active/subscribed state of a calendar
*
* @see calendar_driver::subscribe_calendar()
*/
public function subscribe_calendar($prop)
{
if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
$this->rc->imap_connect();
if ($prop['active'])
return $this->rc->imap->subscribe($cal->get_realname());
else
return $this->rc->imap->unsubscribe($cal->get_realname());
}
return false;
}
/**
* Rename or Create a new IMAP folder
*
* @param array Hash array with calendar properties
*
* @return mixed New folder name or False on failure
*/
private function folder_update(&$prop)
{
$folder = rcube_charset_convert($prop['name'], RCMAIL_CHARSET, 'UTF7-IMAP');
$oldfolder = $prop['oldname']; // UTF7
$parent = $prop['parent']; // UTF7
$delimiter = $_SESSION['imap_delimiter'];
if (strlen($oldfolder)) {
$this->rc->imap_connect();
$options = $this->rc->imap->mailbox_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
$this->last_error = 'Invalid folder name';
return false;
}
else if (strlen($folder) > 128) {
$this->last_error = 'Folder name too long';
return false;
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $delimiter) !== false) {
$this->last_error = 'Invalid folder name';
return false;
}
}
}
if (!empty($options) && ($options['protected'] || $options['norename'])) {
$folder = $oldfolder;
}
else if (strlen($parent)) {
$folder = $parent . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
$this->rc->imap_init();
$folder = $this->rc->imap->mod_mailbox($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$this->rc->imap_connect();
$parent_opts = $this->rc->imap->mailbox_info($parent);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts)))
) {
$this->last_error = 'No permission to create folder';
return false;
}
}
// update the folder name
if (strlen($oldfolder)) {
if ($oldfolder != $folder) {
if (!($result = rcube_kolab::folder_rename($oldfolder, $folder)))
$this->last_error = rcube_kolab::$last_error;
}
else
$result = true;
}
// create new folder
else {
if (!($result = rcube_kolab::folder_create($folder, 'event', false)))
$this->last_error = rcube_kolab::$last_error;
}
// save color in METADATA
// TODO: also save 'showalarams' and other properties here
if ($result && $prop['color']) {
if (!($meta_saved = $this->rc->imap->set_metadata($folder, array('/shared/vendor/kolab/color' => $prop['color'])))) // try in shared namespace
$meta_saved = $this->rc->imap->set_metadata($folder, array('/private/vendor/kolab/color' => $prop['color'])); // try in private namespace
if ($meta_saved)
unset($prop['color']); // unsetting will prevent fallback to local user prefs
}
return $result ? $folder : false;
}
/**
* Delete the given calendar with all its contents
*
* @see calendar_driver::remove_calendar()
*/
public function remove_calendar($prop)
{
if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
$folder = $cal->get_realname();
if (rcube_kolab::folder_delete($folder)) {
// remove color in user prefs (temp. solution)
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
unset($prefs['kolab_calendars'][$prop['id']]);
$this->rc->user->save_prefs($prefs);
return true;
}
else
$this->last_error = rcube_kolab::$last_error;
}
return false;
}
/**
* Fetch a single event
*
* @see calendar_driver::get_event()
* @return array Hash array with event properties, false if not found
*/
public function get_event($event, $writeable = null)
{
if (is_array($event)) {
$id = $event['id'] ? $event['id'] : $event['uid'];
$cal = $event['calendar'];
}
else {
$id = $event;
}
if ($cal && ($storage = $this->calendars[$cal])) {
return $storage->get_event($id);
}
// iterate over all calendar folders and search for the event ID
else if (!$cal) {
foreach ($this->calendars as $storage) {
if ($writeable && $storage->readonly)
continue;
if ($result = $storage->get_event($id)) {
return $result;
}
}
}
return false;
}
/**
* Add a single event to the database
*
* @see calendar_driver::new_event()
*/
public function new_event($event)
{
$cid = $event['calendar'] ? $event['calendar'] : reset(array_keys($this->calendars));
if ($storage = $this->calendars[$cid]) {
// handle attachments to add
if (!empty($event['attachments'])) {
foreach ($event['attachments'] as $idx => $attachment) {
// we'll read file contacts into memory, Horde/Kolab classes does the same
// So we cannot save memory, rcube_imap class can do this better
$event['attachments'][$idx]['content'] = $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']);
}
}
$GLOBALS['conf']['kolab']['no_triggering'] = true;
$success = $storage->insert_event($event);
if ($success)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
return false;
}
/**
* Update an event entry with the given data
*
* @see calendar_driver::new_event()
* @return boolean True on success, False on error
*/
public function edit_event($event)
{
return $this->update_event($event);
}
/**
* Move a single event
*
* @see calendar_driver::move_event()
* @return boolean True on success, False on error
*/
public function move_event($event)
{
if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id'])))
return $this->update_event($event + $ev);
return false;
}
/**
* Resize a single event
*
* @see calendar_driver::resize_event()
* @return boolean True on success, False on error
*/
public function resize_event($event)
{
if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id'])))
return $this->update_event($event + $ev);
return false;
}
/**
* Remove a single event
*
* @param array Hash array with event properties:
* id: Event identifier
* @param boolean Remove record(s) irreversible (mark as deleted otherwise)
*
* @return boolean True on success, False on error
*/
public function remove_event($event, $force = true)
{
$success = false;
$_savemode = $event['_savemode'];
if (($storage = $this->calendars[$event['calendar']]) && ($event = $storage->get_event($event['id']))) {
$event['_savemode'] = $_savemode;
$savemode = 'all';
$master = $event;
$this->rc->session->remove('calendar_restore_event_data');
$GLOBALS['conf']['kolab']['no_triggering'] = true;
// read master if deleting a recurring event
if ($event['recurrence'] || $event['recurrence_id']) {
$master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event;
$savemode = $event['_savemode'];
}
switch ($savemode) {
case 'current':
$_SESSION['calendar_restore_event_data'] = $master;
// removing the first instance => just move to next occurence
if ($master['id'] == $event['id']) {
$recurring = reset($storage->_get_recurring_events($event, $event['start'], $event['end'] + 86400 * 370, $event['id'].'-1'));
$master['start'] = $recurring['start'];
$master['end'] = $recurring['end'];
if ($master['recurrence']['COUNT'])
$master['recurrence']['COUNT']--;
}
else { // add exception to master event
$master['recurrence']['EXDATE'][] = $event['start'];
}
$success = $storage->update_event($master);
break;
case 'future':
if ($master['id'] != $event['id']) {
$_SESSION['calendar_restore_event_data'] = $master;
// set until-date on master event
$master['recurrence']['UNTIL'] = $event['start'] - 86400;
unset($master['recurrence']['COUNT']);
$success = $storage->update_event($master);
break;
}
default: // 'all' is default
$success = $storage->delete_event($master, $force);
break;
}
}
if ($success)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
/**
* Restore a single deleted event
*
* @param array Hash array with event properties:
* id: Event identifier
* @return boolean True on success, False on error
*/
public function restore_event($event)
{
if ($storage = $this->calendars[$event['calendar']]) {
if (!empty($_SESSION['calendar_restore_event_data']))
$success = $storage->update_event($_SESSION['calendar_restore_event_data']);
else
$success = $storage->restore_event($event);
if ($success)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
return false;
}
/**
* Wrapper to update an event object depending on the given savemode
*/
private function update_event($event)
{
if (!($storage = $this->calendars[$event['calendar']]))
return false;
// move event to another folder/calendar
if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) {
if (!($fromcalendar = $this->calendars[$event['_fromcalendar']]))
return false;
if ($event['_savemode'] != 'new') {
if (!$fromcalendar->storage->move($event['id'], $storage->get_realname()))
return false;
$fromcalendar = $storage;
$storage->storage->synchronize();
}
}
else
$fromcalendar = $storage;
$success = false;
$savemode = 'all';
$attachments = array();
$old = $master = $fromcalendar->get_event($event['id']);
// delete existing attachment(s)
if (!empty($event['deleted_attachments'])) {
foreach ($event['deleted_attachments'] as $attachment) {
if (!empty($old['attachments'])) {
foreach ($old['attachments'] as $idx => $att) {
if ($att['id'] == $attachment) {
unset($old['attachments'][$idx]);
}
}
}
}
}
// handle attachments to add
if (!empty($event['attachments'])) {
foreach ($event['attachments'] as $attachment) {
// skip entries without content (could be existing ones)
if (!$attachment['data'] && !$attachment['path'])
continue;
// we'll read file contacts into memory, Horde/Kolab classes does the same
// So we cannot save memory, rcube_imap class can do this better
$attachments[] = array(
'name' => $attachment['name'],
'type' => $attachment['mimetype'],
'content' => $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']),
);
}
}
$event['attachments'] = array_merge((array)$old['attachments'], $attachments);
// modify a recurring event, check submitted savemode to do the right things
if ($old['recurrence'] || $old['recurrence_id']) {
$master = $old['recurrence_id'] ? $fromcalendar->get_event($old['recurrence_id']) : $old;
$savemode = $event['_savemode'];
}
// keep saved exceptions (not submitted by the client)
if ($old['recurrence']['EXDATE'])
$event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
$GLOBALS['conf']['kolab']['no_triggering'] = true;
switch ($savemode) {
case 'new':
// save submitted data as new (non-recurring) event
$event['recurrence'] = array();
$event['uid'] = $this->cal->generate_uid();
// copy attachment data to new event
foreach ((array)$event['attachments'] as $idx => $attachment) {
if (!$attachment['data'])
$attachment['data'] = $fromcalendar->get_attachment_body($attachment['id']);
}
$success = $storage->insert_event($event);
break;
case 'current':
// add exception to master event
$master['recurrence']['EXDATE'][] = $old['start'];
$storage->update_event($master);
// insert new event for this occurence
$event += $old;
$event['recurrence'] = array();
$event['uid'] = $this->cal->generate_uid();
$success = $storage->insert_event($event);
break;
case 'future':
if ($master['id'] != $event['id']) {
// set until-date on master event
$master['recurrence']['UNTIL'] = $old['start'] - 86400;
unset($master['recurrence']['COUNT']);
$storage->update_event($master);
// save this instance as new recurring event
$event += $old;
$event['uid'] = $this->cal->generate_uid();
// if recurrence COUNT, update value to the correct number of future occurences
if ($event['recurrence']['COUNT']) {
$event['recurrence']['COUNT'] -= $old['_instance'];
}
// remove fixed weekday, will be re-set to the new weekday in kolab_calendar::insert_event()
if (strlen($event['recurrence']['BYDAY']) == 2)
unset($event['recurrence']['BYDAY']);
if ($master['recurrence']['BYMONTH'] == gmdate('n', $master['start']))
unset($event['recurrence']['BYMONTH']);
$success = $storage->insert_event($event);
break;
}
default: // 'all' is default
$event['id'] = $master['id'];
$event['uid'] = $master['uid'];
// use start date from master but try to be smart on time or duration changes
$old_start_date = date('Y-m-d', $old['start']);
$old_start_time = date('H:i', $old['start']);
$old_duration = $old['end'] - $old['start'];
$new_start_date = date('Y-m-d', $event['start']);
$new_start_time = date('H:i', $event['start']);
$new_duration = $event['end'] - $event['start'];
$diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
// shifted or resized
if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
$event['start'] = $master['start'] + ($event['start'] - $old['start']);
$event['end'] = $event['start'] + $new_duration;
// remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event()
if ($old_start_date != $new_start_date) {
if (strlen($event['recurrence']['BYDAY']) == 2)
unset($event['recurrence']['BYDAY']);
if ($old['recurrence']['BYMONTH'] == gmdate('n', $old['start']))
unset($event['recurrence']['BYMONTH']);
}
}
$success = $storage->update_event($event);
break;
}
if ($success)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
/**
* Get events from source.
*
* @param integer Event's new start (unix timestamp)
* @param integer Event's new end (unix timestamp)
* @param string Search query (optional)
* @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
* @param boolean Strip virtual events (optional)
* @return array A list of event records
*/
public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1)
{
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
$events = $categories = array();
foreach ($this->calendars as $cid => $calendar) {
if ($calendars && !in_array($cid, $calendars))
continue;
$events = array_merge($events, $this->calendars[$cid]->list_events($start, $end, $search, $virtual));
$categories += $this->calendars[$cid]->categories;
}
// add new categories to user prefs
$old_categories = $this->rc->config->get('calendar_categories', array());
if ($newcats = array_diff(array_map('strtolower', array_keys($categories)), array_map('strtolower', array_keys($old_categories)))) {
foreach ($newcats as $category)
$old_categories[$category] = ''; // no color set yet
$this->rc->user->save_prefs(array('calendar_categories' => $old_categories));
}
return $events;
}
/**
* Get a list of pending alarms to be displayed to the user
*
* @see calendar_driver::pending_alarms()
*/
public function pending_alarms($time, $calendars = null)
{
$interval = 300;
$time -= $time % 60;
$slot = $time;
$slot -= $slot % $interval;
$last = $time - max(60, $this->rc->session->get_keep_alive());
$last -= $last % $interval;
// only check for alerts once in 5 minutes
if ($last == $slot)
return false;
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
$time = $slot + $interval;
$events = array();
foreach ($this->calendars as $cid => $calendar) {
// skip calendars with alarms disabled
if (!$calendar->alarms || ($calendars && !in_array($cid, $calendars)))
continue;
foreach ($calendar->list_events($time, $time + 86400 * 365) as $e) {
// add to list if alarm is set
if ($e['_alarm'] && ($notifyat = $e['start'] - $e['_alarm'] * 60) <= $time) {
$id = $e['id'];
$events[$id] = $e;
$events[$id]['notifyat'] = $notifyat;
}
}
}
// get alarm information stored in local database
if (!empty($events)) {
$event_ids = array_map(array($this->rc->db, 'quote'), array_keys($events));
$result = $this->rc->db->query(sprintf(
"SELECT * FROM kolab_alarms
WHERE event_id IN (%s)",
join(',', $event_ids),
$this->rc->db->now()
));
while ($result && ($e = $this->rc->db->fetch_assoc($result))) {
$dbdata[$e['event_id']] = $e;
}
}
$alarms = array();
foreach ($events as $id => $e) {
// skip dismissed
if ($dbdata[$id]['dismissed'])
continue;
// snooze function may have shifted alarm time
$notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $e['notifyat'];
if ($notifyat <= $time)
$alarms[] = $e;
}
return $alarms;
}
/**
* Feedback after showing/sending an alarm notification
*
* @see calendar_driver::dismiss_alarm()
*/
public function dismiss_alarm($event_id, $snooze = 0)
{
// set new notifyat time or unset if not snoozed
$notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
$query = $this->rc->db->query(
"REPLACE INTO kolab_alarms
(event_id, dismissed, notifyat)
VALUES(?, ?, ?)",
$event_id,
$snooze > 0 ? 0 : 1,
$notifyat
);
return $this->rc->db->affected_rows($query);
}
/**
* List attachments from the given event
*/
public function list_attachments($event)
{
if (!($storage = $this->calendars[$event['calendar']]))
return false;
$event = $storage->get_event($event['id']);
return $event['attachments'];
}
/**
* Get attachment properties
*/
public function get_attachment($id, $event)
{
if (!($storage = $this->calendars[$event['calendar']]))
return false;
$event = $storage->get_event($event['id']);
if ($event && !empty($event['attachments'])) {
foreach ($event['attachments'] as $att) {
if ($att['id'] == $id) {
return $att;
}
}
}
return null;
}
/**
* Get attachment body
*/
public function get_attachment_body($id, $event)
{
if (!($storage = $this->calendars[$event['calendar']]))
return false;
return $storage->get_attachment_body($id);
}
/**
* List availabale categories
* The default implementation reads them from config/user prefs
*/
public function list_categories()
{
// FIXME: complete list with categories saved in config objects (KEP:12)
return $this->rc->config->get('calendar_categories', array());
}
/**
* Fetch free/busy information from a person within the given range
*/
public function get_freebusy_list($email, $start, $end)
{
require_once('Horde/iCalendar.php');
if (empty($email)/* || $end < time()*/)
return false;
// map vcalendar fbtypes to internal values
$fbtypemap = array(
'FREE' => calendar::FREEBUSY_FREE,
'BUSY-TENTATIVE' => calendar::FREEBUSY_TENTATIVE,
'X-OUT-OF-OFFICE' => calendar::FREEBUSY_OOF,
'OOF' => calendar::FREEBUSY_OOF);
// ask kolab server first
$fbdata = @file_get_contents(rcube_kolab::get_freebusy_url($email));
// get free-busy url from contacts
if (!$fbdata) {
$fburl = null;
foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $book) {
$abook = $this->rc->get_address_book($book);
if ($result = $abook->search(array('email'), $email, true, true, true/*, 'freebusyurl'*/)) {
while ($contact = $result->iterate()) {
if ($fburl = $contact['freebusyurl']) {
$fbdata = @file_get_contents($fburl);
break;
}
}
}
if ($fbdata)
break;
}
}
// parse free-busy information using Horde classes
if ($fbdata) {
$fbcal = new Horde_iCalendar;
$fbcal->parsevCalendar($fbdata);
if ($fb = $fbcal->findComponent('vfreebusy')) {
$result = array();
$params = $fb->getExtraParams();
foreach ($fb->getBusyPeriods() as $from => $to) {
if ($to == null) // no information, assume free
break;
$type = $params[$from]['FBTYPE'];
$result[] = array($from, $to, isset($fbtypemap[$type]) ? $fbtypemap[$type] : calendar::FREEBUSY_BUSY);
}
// set period from $start till the begin of the free-busy information as 'unknown'
if (($fbstart = $fb->getStart()) && $start < $fbstart) {
array_unshift($result, array($start, $fbstart, calendar::FREEBUSY_UNKNOWN));
}
// pad period till $end with status 'unknown'
if (($fbend = $fb->getEnd()) && $fbend < $end) {
$result[] = array($fbend, $end, calendar::FREEBUSY_UNKNOWN);
}
return $result;
}
}
return false;
}
/**
* Handler to push folder triggers when sent from client.
* Used to push free-busy changes asynchronously after updating an event
*/
public function push_freebusy()
{
// make shure triggering completes
set_time_limit(0);
ignore_user_abort(true);
$cal = get_input_value('source', RCUBE_INPUT_GPC);
if (!($storage = $this->calendars[$cal]))
return false;
// trigger updates on folder
$folder = $storage->get_folder();
$trigger = $folder->trigger();
if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) {
raise_error(array(
'code' => 900, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed triggering folder. Error was " . $trigger->getMessage()),
true, false);
}
exit;
}
/**
* Callback function to produce driver-specific calendar create/edit form
*
* @param string Request action 'form-edit|form-new'
* @param array Calendar properties (e.g. id, color)
* @param array Edit form fields
*
* @return string HTML content of the form
*/
public function calendar_form($action, $calendar, $formfields)
{
if ($calendar['id'] && ($cal = $this->calendars[$calendar['id']])) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
}
else {
$folder = '';
$color = '';
}
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder);
$delim = $_SESSION['imap_delimiter'];
$form = array();
if (strlen($folder)) {
$path_imap = explode($delim, $folder);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
$this->rc->imap_connect();
$options = $this->rc->imap->mailbox_info($folder);
}
else {
$path_imap = '';
}
// General tab
$form['props'] = array(
'name' => $this->rc->gettext('properties'),
);
// Disable folder name input
if (!empty($options) && ($options['norename'] || $options['protected'])) {
$input_name = new html_hiddenfield(array('name' => 'name', 'id' => 'calendar-name'));
$formfields['name']['value'] = Q(str_replace($delimiter, ' &raquo; ', rcube_kolab::object_name($folder)))
. $input_name->show($folder);
}
// calendar name (default field)
$form['props']['fieldsets']['location'] = array(
'name' => $this->rc->gettext('location'),
'content' => array(
'name' => $formfields['name']
),
);
if (!empty($options) && ($options['norename'] || $options['protected'])) {
// prevent user from moving folder
$hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
}
else {
$select = rcube_kolab::folder_selector('event', array('name' => 'parent'), $folder);
$form['props']['fieldsets']['location']['content']['path'] = array(
'label' => $this->cal->gettext('parentcalendar'),
'value' => $select->show(strlen($folder) ? $path_imap : ''),
);
}
// calendar color (default field)
$form['props']['fieldsets']['settings'] = array(
'name' => $this->rc->gettext('settings'),
'content' => array(
'color' => $formfields['color'],
'showalarms' => $formfields['showalarms'],
),
);
if ($action != 'form-new') {
$form['sharing'] = array(
'name' => Q($this->cal->gettext('tabsharing')),
'content' => html::tag('iframe', array(
'src' => $this->cal->rc->url(array('_action' => 'calendar-acl', 'id' => $calendar['id'], 'framed' => 1)),
'width' => '100%',
'height' => 350,
'border' => 0,
'style' => 'border:0'),
''),
);
}
$this->form_html = '';
if (is_array($hidden_fields)) {
foreach ($hidden_fields as $field) {
$hiddenfield = new html_hiddenfield($field);
$this->form_html .= $hiddenfield->show() . "\n";
}
}
// Create form output
foreach ($form as $tab) {
if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) {
$content = '';
foreach ($tab['fieldsets'] as $fieldset) {
$subcontent = $this->get_form_part($fieldset);
if ($subcontent) {
$content .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $subcontent) ."\n";
}
}
}
else {
$content = $this->get_form_part($tab);
}
if ($content) {
$this->form_html .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n";
}
}
// Parse form template for skin-dependent stuff
$this->rc->output->add_handler('calendarform', array($this, 'calendar_form_html'));
return $this->rc->output->parse('calendar.kolabform', false, false);
}
/**
* Handler for template object
*/
public function calendar_form_html()
{
return $this->form_html;
}
/**
* Helper function used in calendar_form_content(). Creates a part of the form.
*/
private function get_form_part($form)
{
$content = '';
if (is_array($form['content']) && !empty($form['content'])) {
$table = new html_table(array('cols' => 2));
foreach ($form['content'] as $col => $colprop) {
$colprop['id'] = '_'.$col;
$label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
$table->add('title', sprintf('<label for="%s">%s</label>', $colprop['id'], Q($label)));
$table->add(null, $colprop['value']);
}
$content = $table->show();
}
else {
$content = $form['content'];
}
return $content;
}
/**
* Handler to render ACL form for a calendar folder
*/
public function calendar_acl()
{
$this->rc->output->add_handler('folderacl', array($this, 'calendar_acl_form'));
$this->rc->output->send('calendar.kolabacl');
}
/**
* Handler for ACL form template object
*/
public function calendar_acl_form()
{
$calid = get_input_value('_id', RCUBE_INPUT_GPC);
if ($calid && ($cal = $this->calendars[$calid])) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
}
else {
$folder = '';
$color = '';
}
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder);
$delim = $_SESSION['imap_delimiter'];
$form = array();
if (strlen($folder)) {
$path_imap = explode($delim, $folder);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
$this->rc->imap_connect();
$options = $this->rc->imap->mailbox_info($folder);
// Allow plugins to modify the form content (e.g. with ACL form)
$plugin = $this->rc->plugins->exec_hook('calendar_form_kolab',
array('form' => $form, 'options' => $options, 'name' => $folder));
}
return $plugin['form']['sharing']['content'];
}
/**
* Return a (limited) list of color values to be used for calendar and category coloring
*
* @return mixed List for colors as hex values or false if no presets should be shown
*/
public function get_color_values()
{
// selection from http://msdn.microsoft.com/en-us/library/aa358802%28v=VS.85%29.aspx
return array('000000','006400','2F4F4F','800000','808000','008000',
'008080','000080','800080','4B0082','191970','8B0000','008B8B',
'00008B','8B008B','556B2F','8B4513','228B22','6B8E23','2E8B57',
'B8860B','483D8B','A0522D','0000CD','A52A2A','00CED1','696969',
'20B2AA','9400D3','B22222','C71585','3CB371','D2691E','DC143C',
'DAA520','00FA9A','4682B4','7CFC00','9932CC','FF0000','FF4500',
'FF8C00','FFA500','FFD700','FFFF00','9ACD32','32CD32','00FF00',
'00FF7F','00FFFF','5F9EA0','00BFFF','0000FF','FF00FF','808080',
'708090','CD853F','8A2BE2','778899','FF1493','48D1CC','1E90FF',
'40E0D0','4169E1','6A5ACD','BDB76B','BA55D3','CD5C5C','ADFF2F',
'66CDAA','FF6347','8FBC8B','DA70D6','BC8F8F','9370DB','DB7093',
'FF7F50','6495ED','A9A9A9','F4A460','7B68EE','D2B48C','E9967A',
'DEB887','FF69B4','FA8072','F08080','EE82EE','87CEEB','FFA07A',
'F0E68C','DDA0DD','90EE90','7FFFD4','C0C0C0','87CEFA','B0C4DE',
'98FB98','ADD8E6','B0E0E6','D8BFD8','EEE8AA','AFEEEE','D3D3D3',
'FFDEAD');
}
}
diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php
index f7f8b730..a07c9e54 100644
--- a/plugins/calendar/lib/calendar_ical.php
+++ b/plugins/calendar/lib/calendar_ical.php
@@ -1,457 +1,457 @@
<?php
/**
* iCalendar functions for the Calendar plugin
*
* @version 0.7-beta
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Bogomil "Bogo" Shopov <shopov@kolabsys.com>
*
- * Copyright (C) 2010, Lazlo Westerhof - Netherlands
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Class to parse and build vCalendar (iCalendar) files
*
* Uses the Horde:iCalendar class for parsing. To install:
* > pear channel-discover pear.horde.org
* > pear install horde/Horde_Icalendar
*
*/
class calendar_ical
{
const EOL = "\r\n";
private $rc;
private $cal;
private $timezone = 'Z';
public $method;
public $events = array();
function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
// compose timezone string
if ($cal->timezone) {
$hours = floor($cal->timezone + $cal->dst_active);
$this->timezone = sprintf('%s%02d:%02d', ($hours >= 0 ? '+' : ''), $hours, ($cal->timezone - $hours) * 60);
}
}
/**
* Import events from iCalendar format
*
* @param string vCalendar input
* @param string Input charset (from envelope)
* @return array List of events extracted from the input
*/
public function import($vcal, $charset = RCMAIL_CHARSET)
{
$parser = $this->get_parser();
$parser->parsevCalendar($vcal, 'VCALENDAR', $charset);
$this->method = $parser->getAttributeDefault('METHOD', '');
$this->events = array();
if ($data = $parser->getComponents()) {
foreach ($data as $comp) {
if ($comp->getType() == 'vEvent')
$this->events[] = $this->_to_rcube_format($comp);
}
}
return $this->events;
}
/**
* Read iCalendar events from a file
*
* @param string File path to read from
* @return array List of events extracted from the file
*/
public function import_from_file($filepath)
{
$this->events = array();
$fp = fopen($filepath, 'r');
// check file content first
$begin = fread($fp, 1024);
if (!preg_match('/BEGIN:VCALENDAR/i', $begin))
return $this->events;
$parser = $this->get_parser();
$buffer = '';
fseek($fp, 0);
while (($line = fgets($fp, 2048)) !== false) {
$buffer .= $line;
if (preg_match('/END:VEVENT/i', $line)) {
$parser->parsevCalendar($buffer, 'VCALENDAR', RCMAIL_CHARSET, false);
$buffer = '';
}
}
fclose($fp);
if ($data = $parser->getComponents()) {
foreach ($data as $comp) {
if ($comp->getType() == 'vEvent')
$this->events[] = $this->_to_rcube_format($comp);
}
}
return $this->events;
}
/**
* Load iCal parser from the Horde lib
*/
private function get_parser()
{
// use Horde:iCalendar to parse vcalendar file format
require_once 'Horde/iCalendar.php';
// set target charset for parsed events
$GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET;
return new Horde_iCalendar;
}
/**
* Convert the given File_IMC_Parse_Vcalendar_Event object to the internal event format
*/
private function _to_rcube_format($ve)
{
$event = array(
'uid' => $ve->getAttributeDefault('UID'),
'changed' => $ve->getAttributeDefault('DTSTAMP', 0),
'title' => $ve->getAttributeDefault('SUMMARY'),
'start' => $ve->getAttribute('DTSTART'),
'end' => $ve->getAttribute('DTEND'),
// set defaults
'free_busy' => 'busy',
'priority' => 0,
);
// check for all-day dates
if (is_array($event['start'])) {
// create timestamp at 12:00 in user's timezone
$event['start'] = $this->_date2time($event['start']);
$event['allday'] = true;
}
if (is_array($event['end'])) {
$event['end'] = $this->_date2time($event['end']) - 23 * 3600;
}
// map other attributes to internal fields
$_attendees = array();
foreach ($ve->getAllAttributes() as $attr) {
switch ($attr['name']) {
case 'ORGANIZER':
$organizer = array(
'name' => $attr['params']['CN'],
'email' => preg_replace('/^mailto:/', '', $attr['value']),
'role' => 'ORGANIZER',
'status' => 'ACCEPTED',
);
if (isset($_attendees[$organizer['email']])) {
$i = $_attendees[$organizer['email']];
$event['attendees'][$i]['role'] = $organizer['role'];
}
break;
case 'ATTENDEE':
$attendee = array(
'name' => $attr['params']['CN'],
'email' => preg_replace('/^mailto:/', '', $attr['value']),
'role' => $attr['params']['ROLE'] ? $attr['params']['ROLE'] : 'REQ-PARTICIPANT',
'status' => $attr['params']['PARTSTAT'],
'rsvp' => $attr['params']['RSVP'] == 'TRUE',
);
if ($organizer && $organizer['email'] == $attendee['email'])
$attendee['role'] = 'ORGANIZER';
$event['attendees'][] = $attendee;
$_attendees[$attendee['email']] = count($event['attendees']) - 1;
break;
case 'TRANSP':
$event['free_busy'] = $attr['value'] == 'TRANSPARENT' ? 'free' : 'busy';
break;
case 'STATUS':
if ($attr['value'] == 'TENTATIVE')
$event['free_busy'] == 'tentative';
break;
case 'PRIORITY':
if (is_numeric($attr['value'])) {
$event['priority'] = $attr['value'];
}
break;
case 'RRULE':
// parse recurrence rule attributes
foreach (explode(';', $attr['value']) as $par) {
list($k, $v) = explode('=', $par);
$params[$k] = $v;
}
if ($params['UNTIL'])
$params['UNTIL'] = $ve->_parseDateTime($params['UNTIL']);
if (!$params['INTERVAL'])
$params['INTERVAL'] = 1;
$event['recurrence'] = $params;
break;
case 'EXDATE':
break;
case 'RECURRENCE-ID':
$event['recurrence_id'] = $this->_date2time($attr['value']);
break;
case 'SEQUENCE':
$event['sequence'] = intval($attr['value']);
break;
case 'DESCRIPTION':
case 'LOCATION':
$event[strtolower($attr['name'])] = $attr['value'];
break;
case 'CLASS':
case 'X-CALENDARSERVER-ACCESS':
$sensitivity_map = array('PUBLIC' => 0, 'PRIVATE' => 1, 'CONFIDENTIAL' => 2);
$event['sensitivity'] = $sensitivity_map[$attr['value']];
break;
case 'X-MICROSOFT-CDO-BUSYSTATUS':
if ($attr['value'] == 'OOF')
$event['free_busy'] == 'outofoffice';
else if (in_array($attr['value'], array('FREE', 'BUSY', 'TENTATIVE')))
$event['free_busy'] = strtolower($attr['value']);
break;
}
}
// find alarms
if ($valarm = $ve->findComponent('valarm')) {
$action = 'DISPLAY';
$trigger = null;
foreach ($valarm->getAllAttributes() as $attr) {
switch ($attr['name']) {
case 'TRIGGER':
if ($attr['params']['VALUE'] == 'DATE-TIME') {
$trigger = '@' . $attr['value'];
}
else {
$trigger = $attr['value'];
$offset = abs($trigger);
$unit = 'S';
if ($offset % 86400 == 0) {
$unit = 'D';
$trigger = intval($trigger / 86400);
}
else if ($offset % 3600 == 0) {
$unit = 'H';
$trigger = intval($trigger / 3600);
}
else if ($offset % 60 == 0) {
$unit = 'M';
$trigger = intval($trigger / 60);
}
}
break;
case 'ACTION':
$action = $attr['value'];
break;
}
}
if ($trigger)
$event['alarms'] = $trigger . $unit . ':' . $action;
}
// add organizer to attendees list if not already present
if ($organizer && !isset($_attendees[$organizer['email']]))
array_unshift($event['attendees'], $organizer);
// make sure the event has an UID
if (!$event['uid'])
$event['uid'] = $this->cal->$this->generate_uid();
return $event;
}
/**
* Helper method to correctly interpret an all-day date value
*/
private function _date2time($prop)
{
// create timestamp at 12:00 in user's timezone
return is_array($prop) ? strtotime(sprintf('%04d%02d%02dT120000%s', $prop['year'], $prop['month'], $prop['mday'], $this->timezone)) : $prop;
}
/**
* Free resources by clearing member vars
*/
public function reset()
{
$this->method = '';
$this->events = array();
}
/**
* Export events to iCalendar format
*
* @param array Events as array
* @param string VCalendar method to advertise
* @param boolean Directly send data to stdout instead of returning
* @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
*/
public function export($events, $method = null, $write = false)
{
$ical = "BEGIN:VCALENDAR" . self::EOL;
$ical .= "VERSION:2.0" . self::EOL;
$ical .= "PRODID:-//Roundcube Webmail " . RCMAIL_VERSION . "//NONSGML Calendar//EN" . self::EOL;
$ical .= "CALSCALE:GREGORIAN" . self::EOL;
if ($method)
$ical .= "METHOD:" . strtoupper($method) . self::EOL;
if ($write) {
echo $ical;
$ical = '';
}
foreach ($events as $event) {
$vevent = "BEGIN:VEVENT" . self::EOL;
$vevent .= "UID:" . self::escpape($event['uid']) . self::EOL;
$vevent .= "DTSTAMP:" . gmdate('Ymd\THis\Z', $event['changed'] ? $event['changed'] : time()) . self::EOL;
// correctly set all-day dates
if ($event['allday']) {
$vevent .= "DTSTART;VALUE=DATE:" . gmdate('Ymd', $event['start'] + $this->cal->gmt_offset) . self::EOL;
$vevent .= "DTEND;VALUE=DATE:" . gmdate('Ymd', $event['end'] + $this->cal->gmt_offset + 86400) . self::EOL; // ends the next day
}
else {
$vevent .= "DTSTART:" . gmdate('Ymd\THis\Z', $event['start']) . self::EOL;
$vevent .= "DTEND:" . gmdate('Ymd\THis\Z', $event['end']) . self::EOL;
}
$vevent .= "SUMMARY:" . self::escpape($event['title']) . self::EOL;
$vevent .= "DESCRIPTION:" . self::escpape($event['description']) . self::EOL;
if (!empty($event['attendees'])){
$vevent .= $this->_get_attendees($event['attendees']);
}
if (!empty($event['location'])) {
$vevent .= "LOCATION:" . self::escpape($event['location']) . self::EOL;
}
if ($event['recurrence']) {
$vevent .= "RRULE:" . calendar::to_rrule($event['recurrence'], self::EOL) . self::EOL;
}
if(!empty($event['categories'])) {
$vevent .= "CATEGORIES:" . self::escpape(strtoupper($event['categories'])) . self::EOL;
}
if ($event['sensitivity'] > 0) {
$vevent .= "CLASS:" . ($event['sensitivity'] == 2 ? 'CONFIDENTIAL' : 'PRIVATE') . self::EOL;
}
if ($event['alarms']) {
list($trigger, $action) = explode(':', $event['alarms']);
$val = calendar::parse_alaram_value($trigger);
$vevent .= "BEGIN:VALARM\n";
if ($val[1]) $vevent .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . self::EOL;
else $vevent .= "TRIGGER;VALUE=DATE-TIME:" . gmdate('Ymd\THis\Z', $val[0]) . self::EOL;
if ($action) $vevent .= "ACTION:" . self::escpape(strtoupper($action)) . self::EOL;
$vevent .= "END:VALARM\n";
}
$vevent .= "TRANSP:" . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE') . self::EOL;
if ($event['priority']) {
$vevent .= "PRIORITY:" . $event['priority'] . self::EOL;
}
if ($event['cancelled'])
$vevent .= "STATUS:CANCELLED" . self::EOL;
else if ($event['free_busy'] == 'tentative')
$vevent .= "STATUS:TENTATIVE" . self::EOL;
// TODO: export attachments
$vevent .= "END:VEVENT" . self::EOL;
if ($write)
echo rcube_vcard::rfc2425_fold($vevent);
else
$ical .= $vevent;
}
$ical .= "END:VCALENDAR" . self::EOL;
if ($write) {
echo $ical;
return true;
}
// fold lines to 75 chars
return rcube_vcard::rfc2425_fold($ical);
}
private function escpape($str)
{
return preg_replace('/(?<!\\\\)([\:\;\,\\n\\r])/', '\\\$1', $str);
}
/**
* Construct the orginizer of the event.
* @param Array Attendees and roles
*
*/
private function _get_attendees($ats)
{
$organizer = "";
$attendees = "";
foreach ($ats as $at) {
if ($at['role']=="ORGANIZER") {
//I am an orginizer
$organizer .= "ORGANIZER;";
if (!empty($at['name']))
$organizer .= 'CN="' . $at['name'] . '"';
$organizer .= ":mailto:". $at['email'] . self::EOL;
}
else {
//I am an attendee
$attendees .= "ATTENDEE;ROLE=" . $at['role'] . ";PARTSTAT=" . $at['status'];
if ($at['rsvp'])
$attendees .= ";RSVP=TRUE";
if (!empty($at['name']))
$attendees .= ';CN="' . $at['name'] . '"';
$attendees .= ":mailto:" . $at['email'] . self::EOL;
}
}
return $organizer . $attendees;
}
-}
\ No newline at end of file
+}
diff --git a/plugins/calendar/lib/calendar_itip.php b/plugins/calendar/lib/calendar_itip.php
index 84868a99..22480e5d 100644
--- a/plugins/calendar/lib/calendar_itip.php
+++ b/plugins/calendar/lib/calendar_itip.php
@@ -1,322 +1,322 @@
<?php
/**
* iTIP functions for the Calendar plugin
*
* Class providing functionality to manage iTIP invitations
*
* @version 0.7-beta
* @author Thomas Bruederli <roundcube@gmail.com>
* @package calendar
*
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class calendar_itip
{
private $rc;
private $cal;
private $event;
function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
$this->sender = $this->rc->user->get_identity();
}
/**
* Send an iTip mail message
*
* @param array Event object to send
* @param string iTip method (REQUEST|REPLY|CANCEL)
* @param array Hash array with recipient data (name, email)
* @param string Mail subject
* @param string Mail body text label
* @param object Mail_mime object with message data
* @return boolean True on success, false on failure
*/
public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null)
{
if (!$this->sender['name'])
$this->sender['name'] = $this->sender['email'];
if (!$message)
$message = $this->compose_itip_message($event, $method);
$mailto = rcube_idn_to_ascii($recipient['email']);
$headers = $message->headers();
$headers['To'] = format_email_recipient($mailto, $recipient['name']);
$headers['Subject'] = $this->cal->gettext(array(
'name' => $subject,
'vars' => array('title' => $event['title'], 'name' => $this->sender['name'])
));
// compose a list of all event attendees
$attendees_list = array();
foreach ((array)$event['attendees'] as $attendee) {
$attendees_list[] = ($attendee['name'] && $attendee['email']) ?
$attendee['name'] . ' <' . $attendee['email'] . '>' :
($attendee['name'] ? $attendee['name'] : $attendee['email']);
}
$mailbody = $this->cal->gettext(array(
'name' => $bodytext,
'vars' => array(
'title' => $event['title'],
'date' => $this->cal->event_date_text($event, true),
'attendees' => join(', ', $attendees_list),
'sender' => $this->sender['name'],
'organizer' => $this->sender['name'],
)
));
// append links for direct invitation replies
if ($method == 'REQUEST' && ($token = $this->store_invitation($event, $recipient['email']))) {
$mailbody .= "\n\n" . $this->cal->gettext(array(
'name' => 'invitationattendlinks',
'vars' => array('url' => $this->cal->get_url(array('action' => 'attend', 't' => $token))),
));
}
else if ($method == 'CANCEL') {
$this->cancel_itip_invitation($event);
}
$message->headers($headers, true);
$message->setTXTBody(rcube_message::format_flowed($mailbody, 79));
// finally send the message
return rcmail_deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error);
}
/**
* Helper function to build a Mail_mime object to send an iTip message
*
* @param array Event object to send
* @param string iTip method (REQUEST|REPLY|CANCEL)
* @return object Mail_mime object with message data
*/
public function compose_itip_message($event, $method)
{
$from = rcube_idn_to_ascii($this->sender['email']);
$sender = format_email_recipient($from, $this->sender['name']);
// compose multipart message using PEAR:Mail_Mime
$message = new Mail_mime("\r\n");
$message->setParam('text_encoding', 'quoted-printable');
$message->setParam('head_encoding', 'quoted-printable');
$message->setParam('head_charset', RCMAIL_CHARSET);
$message->setParam('text_charset', RCMAIL_CHARSET . ";\r\n format=flowed");
// compose common headers array
$headers = array(
'From' => $sender,
'Date' => rcmail_user_date(),
'Message-ID' => rcmail_gen_message_id(),
'X-Sender' => $from,
);
if ($agent = $this->rc->config->get('useragent'))
$headers['User-Agent'] = $agent;
$message->headers($headers);
// attach ics file for this event
$ical = $this->cal->get_ical();
$ics = $ical->export(array($event), $method);
$message->addAttachment($ics, 'text/calendar', 'event.ics', false, '8bit', 'attachment', RCMAIL_CHARSET . "; method=" . $method);
return $message;
}
/**
* Find invitation record by token
*
* @param string Invitation token
* @return mixed Invitation record as hash array or False if not found
*/
public function get_invitation($token)
{
if ($parts = $this->decode_token($token)) {
$result = $this->rc->db->query("SELECT * FROM itipinvitations WHERE token=?", $parts['base']);
if ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
$rec['event'] = unserialize($rec['event']);
$rec['attendee'] = $parts['attendee'];
return $rec;
}
}
return false;
}
/**
* Update the attendee status of the given invitation record
*
* @param array Invitation record as fetched with calendar_itip::get_invitation()
* @param string Attendee email address
* @param string New attendee status
*/
public function update_invitation($invitation, $email, $newstatus)
{
if (is_string($invitation))
$invitation = $this->get_invitation($invitation);
if ($invitation['token'] && $invitation['event']) {
// update attendee record in event data
foreach ($invitation['event']['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
}
else if ($attendee['email'] == $email) {
// nothing to be done here
if ($attendee['status'] == $newstatus)
return true;
$invitation['event']['attendees'][$i]['status'] = $newstatus;
$this->sender = $attendee;
}
}
$invitation['event']['changed'] = time();
// send iTIP REPLY message to organizer
if ($organizer) {
$status = strtolower($newstatus);
if ($this->send_itip_message($invitation['event'], 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->cal->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->cal->gettext('itipresponseerror'), 'error');
}
// update record in DB
$query = $this->rc->db->query(
"UPDATE itipinvitations
SET event=?
WHERE token=?",
self::serialize_event($invitation['event']),
$invitation['token']
);
if ($this->rc->db->affected_rows($query))
return true;
}
return false;
}
/**
* Create iTIP invitation token for later replies via URL
*
* @param array Hash array with event properties
* @param string Attendee email address
* @return string Invitation token
*/
public function store_invitation($event, $attendee)
{
static $stored = array();
if (!$event['uid'] || !$attendee)
return false;
// generate token for this invitation
$token = $this->generate_token($event, $attendee);
$base = substr($token, 0, 40);
// already stored this
if ($stored[$base])
return $token;
$query = $this->rc->db->query(
"REPLACE INTO itipinvitations
(token, event_uid, user_id, event, expires)
VALUES(?, ?, ?, ?, ?)",
$base,
$event['uid'],
$this->rc->user->ID,
self::serialize_event($event),
date('Y-m-d H:i:s', $event['end'] + 86400 * 2)
);
if ($this->rc->db->affected_rows($query)) {
$stored[$base] = 1;
return $token;
}
return false;
}
/**
* Mark invitations for the given event as cancelled
*
* @param array Hash array with event properties
*/
public function cancel_itip_invitation($event)
{
// flag invitation record as cancelled
$this->rc->db->query(
"UPDATE itipinvitations
SET cancelled=1
WHERE event_uid=? AND user_id=?",
$event['uid'],
$this->rc->user->ID
);
}
/**
* Generate an invitation request token for the given event and attendee
*
* @param array Event hash array
* @param string Attendee email address
*/
public function generate_token($event, $attendee)
{
$base = sha1($event['uid'] . ';' . $this->rc->user->ID);
$mail = base64_encode($attendee);
$hash = substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6);
return "$base.$mail.$hash";
}
/**
* Decode the given iTIP request token and return its parts
*
* @param string Request token to decode
* @return mixed Hash array with parts or False if invalid
*/
public function decode_token($token)
{
list($base, $mail, $hash) = explode('.', $token);
// validate and return parts
if ($mail && $hash && $hash == substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6)) {
return array('base' => $base, 'attendee' => base64_decode($mail));
}
return false;
}
/**
* Helper method to serialize the given event for storing in invitations table
*/
private static function serialize_event($event)
{
$ev = $event;
$ev['description'] = abbreviate_string($ev['description'], 100);
unset($ev['attachments']);
return serialize($ev);
}
}
diff --git a/plugins/calendar/lib/calendar_recurrence.php b/plugins/calendar/lib/calendar_recurrence.php
index ae151b74..23f611b8 100644
--- a/plugins/calendar/lib/calendar_recurrence.php
+++ b/plugins/calendar/lib/calendar_recurrence.php
@@ -1,86 +1,86 @@
<?php
/**
* Recurrence computation class for the Calendar plugin
*
* Uitility class to compute instances of recurring events.
*
* @version 0.7-beta
* @author Thomas Bruederli <roundcube@gmail.com>
* @package calendar
*
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class calendar_recurrence
{
private $cal;
private $event;
private $engine;
private $tz_offset = 0;
private $dst_start = false;
private $hour = 0;
/**
* Default constructor
*
* @param object calendar The calendar plugin instance
* @param array The event object to operate on
*/
function __construct($cal, $event)
{
$this->cal = $cal;
// use Horde classes to compute recurring instances
// TODO: replace with something that has less than 6'000 lines of code
require_once($this->cal->home . '/lib/Horde_Date_Recurrence.php');
$this->event = $event;
$this->engine = new Horde_Date_Recurrence($event['start']);
$this->engine->fromRRule20(calendar::to_rrule($event['recurrence']));
if (is_array($event['recurrence']['EXDATE'])) {
foreach ($event['recurrence']['EXDATE'] as $exdate)
$this->engine->addException(date('Y', $exdate), date('n', $exdate), date('j', $exdate));
}
$this->tz_offset = $event['allday'] ? $this->cal->gmt_offset - date('Z') : 0;
$this->next = new Horde_Date($event['start'] + $this->tz_offset); # shift all-day times to server timezone because computation operates in local TZ
$this->dst_start = $this->next->format('I');
$this->hour = $this->next->hour;
}
/**
* Get timestamp of the next occurence of this event
*
* @return mixed Unix timestamp or False if recurrence ended
*/
public function next_start()
{
$time = false;
if ($this->next && ($next = $this->engine->nextActiveRecurrence(array('year' => $this->next->year, 'month' => $this->next->month, 'mday' => $this->next->mday + 1, 'hour' => $this->next->hour, 'min' => $this->next->min, 'sec' => $this->next->sec)))) {
if ($this->event['allday']) {
$next->hour = $this->hour; # fix time for all-day events
$next->min = 0;
}
# $dst_diff = ($this->dst_start - $next->format('I')) * 3600; # consider difference in daylight saving between base event and recurring instance
$time = $next->timestamp() - $this->tz_offset;
$this->next = $next;
}
return $time;
}
}
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index e5baba6a..bc51c614 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -1,815 +1,815 @@
<?php
/**
* User Interface class for the Calendar plugin
*
* @version 0.7-beta
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <roundcube@gmail.com>
*
- * Copyright (C) 2010, Lazlo Westerhof - Netherlands
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class calendar_ui
{
private $rc;
private $cal;
private $ready = false;
public $screen;
function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
$this->screen = $this->rc->task == 'calendar' ? ($this->rc->action ? $this->rc->action: 'calendar') : 'other';
}
/**
* Calendar UI initialization and requests handlers
*/
public function init()
{
if ($this->ready) // already done
return;
// add taskbar button
$this->cal->add_button(array(
'name' => 'calendar',
'class' => 'button-calendar',
'label' => 'calendar.calendar',
'href' => './?_task=calendar',
), 'taskbar');
// load basic client script (which - unfortunately - requires fullcalendar)
$this->cal->include_script('lib/js/fullcalendar.js');
$this->cal->include_script('calendar_base.js');
$skin = $this->rc->config->get('skin');
$this->cal->include_stylesheet('skins/' . $skin . '/calendar.css');
$this->ready = true;
}
/**
* Register handler methods for the template engine
*/
public function init_templates()
{
$this->cal->register_handler('plugin.calendar_css', array($this, 'calendar_css'));
$this->cal->register_handler('plugin.calendar_list', array($this, 'calendar_list'));
$this->cal->register_handler('plugin.calendar_select', array($this, 'calendar_select'));
$this->cal->register_handler('plugin.category_select', array($this, 'category_select'));
$this->cal->register_handler('plugin.freebusy_select', array($this, 'freebusy_select'));
$this->cal->register_handler('plugin.priority_select', array($this, 'priority_select'));
$this->cal->register_handler('plugin.sensitivity_select', array($this, 'sensitivity_select'));
$this->cal->register_handler('plugin.alarm_select', array($this, 'alarm_select'));
$this->cal->register_handler('plugin.snooze_select', array($this, 'snooze_select'));
$this->cal->register_handler('plugin.recurrence_form', array($this, 'recurrence_form'));
$this->cal->register_handler('plugin.attachments_form', array($this, 'attachments_form'));
$this->cal->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
$this->cal->register_handler('plugin.attendees_list', array($this, 'attendees_list'));
$this->cal->register_handler('plugin.attendees_form', array($this, 'attendees_form'));
$this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table'));
$this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
$this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning'));
$this->cal->register_handler('plugin.event_rsvp_buttons', array($this, 'event_rsvp_buttons'));
$this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options'));
$this->cal->register_handler('plugin.events_import_form', array($this, 'events_import_form'));
$this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template
}
/**
* Adds CSS stylesheets to the page header
*/
public function addCSS()
{
$skin = $this->rc->config->get('skin');
$this->cal->include_stylesheet('skins/' . $skin . '/fullcalendar.css');
$this->cal->include_stylesheet('skins/' . $skin . '/jquery.miniColors.css');
}
/**
* Adds JS files to the page header
*/
public function addJS()
{
$this->cal->include_script('calendar_ui.js');
$this->cal->include_script('lib/js/jquery.miniColors.min.js');
}
/**
*
*/
function calendar_css($attrib = array())
{
$mode = $this->rc->config->get('calendar_event_coloring', $this->cal->defaults['calendar_event_coloring']);
$categories = $this->cal->driver->list_categories();
$css = "\n";
foreach ((array)$categories as $class => $color) {
if (empty($color))
continue;
$class = 'cat-' . asciiwords(strtolower($class), true);
$css .= ".$class { color: #$color }\n";
if ($mode > 0) {
if ($mode == 2) {
$css .= ".fc-event-$class .fc-event-bg {";
$css .= " opacity: 0.9;";
$css .= " filter: alpha(opacity=90);";
}
else {
$css .= ".fc-event-$class.fc-event-skin, ";
$css .= ".fc-event-$class .fc-event-skin, ";
$css .= ".fc-event-$class .fc-event-inner {";
}
$css .= " background-color: #" . $color . ";";
if ($mode % 2)
$css .= " border-color: #$color;";
$css .= "}\n";
}
}
$calendars = $this->cal->driver->list_calendars();
foreach ((array)$calendars as $id => $prop) {
if (!$prop['color'])
continue;
$color = $prop['color'];
$class = 'cal-' . asciiwords($id, true);
$css .= "li.$class, #eventshow .$class { color: #$color }\n";
if ($mode != 1) {
if ($mode == 3) {
$css .= ".fc-event-$class .fc-event-bg {";
$css .= " opacity: 0.9;";
$css .= " filter: alpha(opacity=90);";
}
else {
$css .= ".fc-event-$class, ";
$css .= ".fc-event-$class .fc-event-inner {";
}
if (!$attrib['printmode'])
$css .= " background-color: #$color;";
if ($mode % 2 == 0)
$css .= " border-color: #$color;";
$css .= "}\n";
}
}
return html::tag('style', array('type' => 'text/css'), $css);
}
/**
*
*/
function calendar_list($attrib = array())
{
$calendars = $this->cal->driver->list_calendars();
$li = '';
foreach ((array)$calendars as $id => $prop) {
if ($attrib['activeonly'] && !$prop['active'])
continue;
unset($prop['user_id']);
$prop['alarms'] = $this->cal->driver->alarms;
$prop['attendees'] = $this->cal->driver->attendees;
$prop['freebusy'] = $this->cal->driver->freebusy;
$prop['attachments'] = $this->cal->driver->attachments;
$prop['undelete'] = $this->cal->driver->undelete;
$jsenv[$id] = $prop;
$html_id = html_identifier($id);
$class = 'cal-' . asciiwords($id, true);
if ($prop['readonly'])
$class .= ' readonly';
if ($prop['class_name'])
$class .= ' '.$prop['class_name'];
$li .= html::tag('li', array('id' => 'rcmlical' . $html_id, 'class' => $class),
html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') . html::span(null, Q($prop['name'])));
}
$this->rc->output->set_env('calendars', $jsenv);
$this->rc->output->add_gui_object('folderlist', $attrib['id']);
return html::tag('ul', $attrib, $li, html::$common_attrib);
}
/**
*
*/
function angenda_options($attrib = array())
{
$attrib += array('id' => 'agendaoptions');
$attrib['style'] .= 'display:none';
$select_range = new html_select(array('name' => 'listrange', 'id' => 'agenda-listrange'));
$select_range->add(1 . ' ' . preg_replace('/\(.+\)/', '', $this->cal->gettext('days')), $days);
foreach (array(2,5,7,14,30,60,90) as $days)
$select_range->add($days . ' ' . preg_replace('/\(|\)/', '', $this->cal->gettext('days')), $days);
$html .= html::label('agenda-listrange', $this->cal->gettext('listrange'));
$html .= $select_range->show($this->rc->config->get('calendar_agenda_range', $this->cal->defaults['calendar_agenda_range']));
$select_sections = new html_select(array('name' => 'listsections', 'id' => 'agenda-listsections'));
$select_sections->add('---', '');
foreach (array('day' => 'days', 'week' => 'weeks', 'month' => 'months', 'smart' => 'smartsections') as $val => $label)
$select_sections->add(preg_replace('/\(|\)/', '', ucfirst($this->cal->gettext($label))), $val);
$html .= html::span('spacer', '&nbsp');
$html .= html::label('agenda-listsections', $this->cal->gettext('listsections'));
$html .= $select_sections->show($this->rc->config->get('calendar_agenda_sections', $this->cal->defaults['calendar_agenda_sections']));
return html::div($attrib, $html);
}
/**
* Render a HTML select box for calendar selection
*/
function calendar_select($attrib = array())
{
$attrib['name'] = 'calendar';
$select = new html_select($attrib);
foreach ((array)$this->cal->driver->list_calendars() as $id => $prop) {
if (!$prop['readonly'])
$select->add($prop['name'], $id);
}
return $select->show(null);
}
/**
* Render a HTML select box to select an event category
*/
function category_select($attrib = array())
{
$attrib['name'] = 'categories';
$select = new html_select($attrib);
$select->add('---', '');
foreach ((array)$this->cal->driver->list_categories() as $cat => $color) {
$select->add($cat, $cat);
}
return $select->show(null);
}
/**
* Render a HTML select box for free/busy/out-of-office property
*/
function freebusy_select($attrib = array())
{
$attrib['name'] = 'freebusy';
$select = new html_select($attrib);
$select->add($this->cal->gettext('free'), 'free');
$select->add($this->cal->gettext('busy'), 'busy');
$select->add($this->cal->gettext('outofoffice'), 'outofoffice');
$select->add($this->cal->gettext('tentative'), 'tentative');
return $select->show(null);
}
/**
* Render a HTML select for event priorities
*/
function priority_select($attrib = array())
{
$attrib['name'] = 'priority';
$select = new html_select($attrib);
$select->add('---', '0');
$select->add('1 '.$this->cal->gettext('highest'), '1');
$select->add('2 '.$this->cal->gettext('high'), '2');
$select->add('3 ', '3');
$select->add('4 ', '4');
$select->add('5 '.$this->cal->gettext('normal'), '5');
$select->add('6 ', '6');
$select->add('7 ', '7');
$select->add('8 '.$this->cal->gettext('low'), '8');
$select->add('9 '.$this->cal->gettext('lowest'), '9');
return $select->show(null);
}
/**
* Render HTML input for sensitivity selection
*/
function sensitivity_select($attrib = array())
{
$attrib['name'] = 'sensitivity';
$select = new html_select($attrib);
$select->add($this->cal->gettext('public'), '0');
$select->add($this->cal->gettext('private'), '1');
$select->add($this->cal->gettext('confidential'), '2');
return $select->show(null);
}
/**
* Render HTML form for alarm configuration
*/
function alarm_select($attrib = array())
{
unset($attrib['name']);
$select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type'));
$select_type->add($this->cal->gettext('none'), '');
foreach ($this->cal->driver->alarm_types as $type)
$select_type->add($this->cal->gettext(strtolower("alarm{$type}option")), $type);
$input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3));
$input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10));
$input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6));
$select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset'));
foreach (array('-M','-H','-D','+M','+H','+D','@') as $trigger)
$select_offset->add($this->cal->gettext('trigger' . $trigger), $trigger);
// pre-set with default values from user settings
$preset = calendar::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
$hidden = array('style' => 'display:none');
$html = html::span('edit-alarm-set',
$select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' .
html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'),
$input_value->show($preset[0]) . ' ' .
$select_offset->show($preset[1]) . ' ' .
$input_date->show('', $hidden) . ' ' .
$input_time->show('', $hidden)
)
);
// TODO: support adding more alarms
#$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->cal->gettext('addalarm')),
# $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)');
return $html;
}
function snooze_select($attrib = array())
{
$steps = array(
5 => 'repeatinmin',
10 => 'repeatinmin',
15 => 'repeatinmin',
20 => 'repeatinmin',
30 => 'repeatinmin',
60 => 'repeatinhr',
120 => 'repeatinhrs',
1440 => 'repeattomorrow',
10080 => 'repeatinweek',
);
$items = array();
foreach ($steps as $n => $label) {
$items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'),
$this->cal->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60))))));
}
return html::tag('ul', $attrib, join("\n", $items), html::$common_attrib);
}
/**
*
*/
function edit_attendees_notify($attrib = array())
{
$checkbox = new html_checkbox(array('name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1));
return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('sendnotifications')));
}
/**
* Generate the form for recurrence settings
*/
function recurring_event_warning($attrib = array())
{
$attrib['id'] = 'edit-recurring-warning';
$radio = new html_radiobutton(array('name' => '_savemode', 'class' => 'edit-recurring-savemode'));
$form = html::label(null, $radio->show('', array('value' => 'current')) . $this->cal->gettext('currentevent')) . ' ' .
html::label(null, $radio->show('', array('value' => 'future')) . $this->cal->gettext('futurevents')) . ' ' .
html::label(null, $radio->show('all', array('value' => 'all')) . $this->cal->gettext('allevents')) . ' ' .
html::label(null, $radio->show('', array('value' => 'new')) . $this->cal->gettext('saveasnew'));
return html::div($attrib, html::div('message', html::span('ui-icon ui-icon-alert', '') . $this->cal->gettext('changerecurringeventwarning')) . html::div('savemode', $form));
}
/**
* Generate the form for recurrence settings
*/
function recurrence_form($attrib = array())
{
switch ($attrib['part']) {
// frequency selector
case 'frequency':
$select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency'));
$select->add($this->cal->gettext('never'), '');
$select->add($this->cal->gettext('daily'), 'DAILY');
$select->add($this->cal->gettext('weekly'), 'WEEKLY');
$select->add($this->cal->gettext('monthly'), 'MONTHLY');
$select->add($this->cal->gettext('yearly'), 'YEARLY');
$html = html::label('edit-frequency', $this->cal->gettext('frequency')) . $select->show('');
break;
// daily recurrence
case 'daily':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-daily'));
$html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('days')));
break;
// weekly recurrence form
case 'weekly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-weekly'));
$html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('weeks')));
// weekday selection
$daymap = array('sun','mon','tue','wed','thu','fri','sat');
$checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday'));
$first = $this->rc->config->get('calendar_first_day', 1);
for ($weekdays = '', $j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$weekdays .= html::label(array('class' => 'weekday'), $checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) . $this->cal->gettext($daymap[$d])) . ' ';
}
$html .= html::div($attrib, html::label(null, $this->cal->gettext('bydays')) . $weekdays);
break;
// monthly recurrence form
case 'monthly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly'));
$html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('months')));
/* multiple month selection is not supported by Kolab
$checkbox = new html_radiobutton(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
for ($monthdays = '', $d = 1; $d <= 31; $d++) {
$monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d);
$monthdays .= $d % 7 ? ' ' : html::br();
}
*/
// rule selectors
$radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode'));
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
$table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->cal->gettext('onsamedate'))); // $this->cal->gettext('each')
$table->add(null, $monthdays);
$table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->cal->gettext('onevery')));
$table->add(null, $this->rrule_selectors($attrib['part']));
$html .= html::div($attrib, $table->show());
break;
// annually recurrence form
case 'yearly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-yearly'));
$html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('years')));
// month selector
$monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec');
$boxtype = is_a($this->cal->driver, 'kolab_driver') ? 'radio' : 'checkbox';
$checkbox = new html_inputfield(array('type' => $boxtype, 'name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
for ($months = '', $m = 1; $m <= 12; $m++) {
$months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->cal->gettext($monthmap[$m]));
$months .= $m % 4 ? ' ' : html::br();
}
$html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months);
// day rule selection
$html .= html::div($attrib, html::label(null, $this->cal->gettext('onevery')) . $this->rrule_selectors($attrib['part'], '---'));
break;
// end of recurrence form
case 'until':
$radio = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until'));
$select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times'));
$input = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => "10"));
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
$table->add('label', ucfirst($this->cal->gettext('recurrencend')));
$table->add(null, html::label(null, $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever')) . ' ' .
$this->cal->gettext('forever')));
$table->add('label', '');
$table->add(null, $radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count')) . ' ' .
$this->cal->gettext(array(
'name' => 'forntimes',
'vars' => array('nr' => $select->show(1)))
));
$table->add('label', '');
$table->add(null, $radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until')) . ' ' .
$this->cal->gettext('untildate') . ' ' . $input->show(''));
$html = $table->show();
break;
}
return $html;
}
/**
* Input field for interval selection
*/
private function interval_selector($attrib)
{
$select = new html_select($attrib);
$select->add(range(1,30), range(1,30));
return $select;
}
/**
* Drop-down menus for recurrence rules like "each last sunday of"
*/
private function rrule_selectors($part, $noselect = null)
{
// rule selectors
$select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix"));
if ($noselect) $select_prefix->add($noselect, '');
$select_prefix->add(array(
$this->cal->gettext('first'),
$this->cal->gettext('second'),
$this->cal->gettext('third'),
$this->cal->gettext('fourth')
),
array(1, 2, 3, 4));
// Kolab doesn't support 'last' but others do.
if (!is_a($this->cal->driver, 'kolab_driver'))
$select_prefix->add($this->cal->gettext('last'), -1);
$select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday"));
if ($noselect) $select_wday->add($noselect, '');
$daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday');
$first = $this->rc->config->get('calendar_first_day', 1);
for ($j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$select_wday->add($this->cal->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2)));
}
if ($part == 'monthly')
$select_wday->add($this->cal->gettext('dayofmonth'), '');
return $select_prefix->show() . '&nbsp;' . $select_wday->show();
}
/**
* Form for uploading and importing events
*/
function events_import_form($attrib = array())
{
if (!$attrib['id'])
$attrib['id'] = 'rcmImportForm';
// Get max filesize, enable upload progress bar
$max_filesize = rcube_upload_init();
$button = new html_inputfield(array('type' => 'button'));
$input = new html_inputfield(array(
'type' => 'file', 'name' => '_data', 'size' => $attrib['uploadfieldsize']));
$select = new html_select(array('name' => '_range', 'id' => 'event-import-range'));
$select->add(array(
$this->cal->gettext('onemonthback'),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))),
$this->cal->gettext('all'),
),
array('1','2','6','12',0));
$html .= html::div('form-section',
html::div(null, $input->show()) .
html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
);
$html .= html::div('form-section',
html::label('event-import-calendar', $this->cal->gettext('calendar')) .
$this->calendar_select(array('name' => 'calendar', 'id' => 'event-import-calendar'))
);
$html .= html::div('form-section',
html::label('event-import-range', $this->cal->gettext('importrange')) .
$select->show(1)
);
$this->rc->output->add_gui_object('importform', $attrib['id']);
$this->rc->output->add_label('import');
return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'import_events')),
'method' => "post", 'enctype' => 'multipart/form-data', 'id' => $attrib['id']),
$html
);
}
/**
* Generate the form for event attachments upload
*/
function attachments_form($attrib = array())
{
// add ID if not given
if (!$attrib['id'])
$attrib['id'] = 'rcmUploadForm';
// Get max filesize, enable upload progress bar
$max_filesize = rcube_upload_init();
$button = new html_inputfield(array('type' => 'button'));
$input = new html_inputfield(array(
'type' => 'file', 'name' => '_attachments[]',
'multiple' => 'multiple', 'size' => $attrib['attachmentfieldsize']));
return html::div($attrib,
html::div(null, $input->show()) .
html::div('buttons', $button->show(rcube_label('upload'), array('class' => 'button mainaction',
'onclick' => JS_OBJECT_NAME . ".upload_file(this.form)"))) .
html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
);
}
/**
* Generate HTML element for attachments list
*/
function attachments_list($attrib = array())
{
if (!$attrib['id'])
$attrib['id'] = 'rcmAttachmentList';
$skin_path = $this->rc->config->get('skin_path');
if ($attrib['deleteicon']) {
$_SESSION['calendar_deleteicon'] = $skin_path . $attrib['deleteicon'];
$this->rc->output->set_env('deleteicon', $skin_path . $attrib['deleteicon']);
}
if ($attrib['cancelicon'])
$this->rc->output->set_env('cancelicon', $skin_path . $attrib['cancelicon']);
if ($attrib['loadingicon'])
$this->rc->output->set_env('loadingicon', $skin_path . $attrib['loadingicon']);
$this->rc->output->add_gui_object('attachmentlist', $attrib['id']);
return html::tag('ul', $attrib, '', html::$common_attrib);
}
function attachment_controls($attrib = array())
{
$table = new html_table(array('cols' => 3));
if (!empty($this->cal->attachment['name'])) {
$table->add('title', Q(rcube_label('filename')));
$table->add(null, Q($this->cal->attachment['name']));
$table->add(null, '[' . html::a('?'.str_replace('_frame=', '_download=', $_SERVER['QUERY_STRING']), Q(rcube_label('download'))) . ']');
}
if (!empty($this->cal->attachment['size'])) {
$table->add('title', Q(rcube_label('filesize')));
$table->add(null, Q(show_bytes($this->cal->attachment['size'])));
}
return $table->show($attrib);
}
/**
* Handler for calendar form template.
* The form content could be overriden by the driver
*/
function calendar_editform($action, $calendar = array())
{
// compose default calendar form fields
$input_name = new html_inputfield(array('name' => 'name', 'id' => 'calendar-name', 'size' => 20));
$input_color = new html_inputfield(array('name' => 'color', 'id' => 'calendar-color', 'size' => 6));
$formfields = array(
'name' => array(
'label' => $this->cal->gettext('name'),
'value' => $input_name->show($name),
'id' => 'calendar-name',
),
'color' => array(
'label' => $this->cal->gettext('color'),
'value' => $input_color->show($calendar['color']),
'id' => 'calendar-color',
),
);
if ($this->cal->driver->alarms) {
$checkbox = new html_checkbox(array('name' => 'showalarms', 'id' => 'calendar-showalarms', 'value' => 1));
$formfields['showalarms'] = array(
'label' => $this->cal->gettext('showalarms'),
'value' => $checkbox->show($calendar['showalarms']?1:0),
'id' => 'calendar-showalarms',
);
}
// allow driver to extend or replace the form content
return html::tag('form', array('action' => "#", 'method' => "get", 'id' => 'calendarpropform'),
$this->cal->driver->calendar_form($action, $calendar, $formfields)
);
}
/**
*
*/
function attendees_list($attrib = array())
{
$table = new html_table(array('cols' => 5, 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable'));
$table->add_header('role', $this->cal->gettext('role'));
$table->add_header('name', $this->cal->gettext('attendee'));
$table->add_header('availability', $this->cal->gettext('availability'));
$table->add_header('confirmstate', $this->cal->gettext('confirmstate'));
$table->add_header('options', '');
return $table->show($attrib);
}
/**
*
*/
function attendees_form($attrib = array())
{
$input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30));
$checkbox = new html_checkbox(array('name' => 'invite', 'id' => 'edit-attendees-invite', 'value' => 1));
return html::div($attrib,
html::div(null, $input->show() . " " .
html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->cal->gettext('addattendee'))) . " " .
html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->cal->gettext('scheduletime').'...'))) .
html::p('attendees-invitebox', html::label(null, $checkbox->show(1) . $this->cal->gettext('sendinvitations')))
);
}
/**
*
*/
function attendees_freebusy_table($attrib = array())
{
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellspacing' => 0));
$table->add('attendees',
html::tag('h3', 'boxtitle', $this->cal->gettext('tabattendees')) .
html::div('timesheader', '&nbsp;') .
html::div(array('id' => 'schedule-attendees-list', 'class' => 'attendees-list'), '')
);
$table->add('times',
html::div('scroll',
html::tag('table', array('id' => 'schedule-freebusy-times', 'border' => 0, 'cellspacing' => 0), html::tag('thead') . html::tag('tbody')) .
html::div(array('id' => 'schedule-event-time', 'style' => 'display:none'), '&nbsp;')
)
);
return $table->show($attrib);
}
/**
* Render event details in a table
*/
function event_details_table($event, $title)
{
$table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails'));
$table->add('ititle', $title);
$table->add('title', Q($event['title']));
$table->add('label', $this->cal->gettext('date'));
$table->add('location', Q($this->cal->event_date_text($event)));
if ($event['location']) {
$table->add('label', $this->cal->gettext('location'));
$table->add('location', Q($event['location']));
}
return $table->show();
}
/**
*
*/
function event_invitebox($attrib = array())
{
if ($this->cal->event) {
return html::div($attrib,
$this->event_details_table($this->cal->event, $this->cal->gettext('itipinvitation')) .
$this->cal->invitestatus
);
}
return '';
}
function event_rsvp_buttons($attrib = array())
{
$attrib += array('type' => 'button');
foreach (array('accepted','tentative','declined') as $method) {
$buttons .= html::tag('input', array(
'type' => $attrib['type'],
'name' => $attrib['iname'],
'class' => 'button',
'rel' => $method,
'value' => $this->cal->gettext('itip' . $method),
));
}
return html::div($attrib,
html::div('label', $this->cal->gettext('acceptinvitation')) .
html::div('rsvp-buttons', $buttons));
}
}
diff --git a/plugins/calendar/print.js b/plugins/calendar/print.js
index 6211647c..88df9126 100644
--- a/plugins/calendar/print.js
+++ b/plugins/calendar/print.js
@@ -1,159 +1,168 @@
/**
* Print view for the Calendar plugin
*
* @version 0.7-beta
* @author Thomas Bruederli <roundcube@gmail.com>
*
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* calendar plugin printing code */
window.rcmail && rcmail.addEventListener('init', function(evt) {
// quote html entities
var Q = function(str)
{
return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
};
var rc_loading;
var showdesc = true;
var settings = rcmail.env.calendar_settings;
// create list of event sources AKA calendars
var src, event_sources = [];
var add_url = (rcmail.env.search ? '&q='+escape(rcmail.env.search) : '');
for (var id in rcmail.env.calendars) {
source = $.extend({
url: "./?_task=calendar&_action=load_events&source=" + escape(id) + add_url,
className: 'fc-event-cal-'+id,
id: id
}, rcmail.env.calendars[id]);
event_sources.push(source);
}
var viewdate = new Date();
if (rcmail.env.date)
viewdate.setTime(rcmail.env.date * 1000);
// initalize the fullCalendar plugin
var fc = $('#calendar').fullCalendar({
header: {
left: '',
center: 'title',
right: 'agendaDay,agendaWeek,month,table'
},
aspectRatio: 0.85,
ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone
date: viewdate.getDate(),
month: viewdate.getMonth(),
year: viewdate.getFullYear(),
defaultView: rcmail.env.view,
eventSources: event_sources,
monthNames : settings['months'],
monthNamesShort : settings['months_short'],
dayNames : settings['days'],
dayNamesShort : settings['days_short'],
firstDay : settings['first_day'],
firstHour : settings['first_hour'],
slotMinutes : 60/settings['timeslots'],
timeFormat: {
'': settings['time_format'],
agenda: settings['time_format'] + '{ - ' + settings['time_format'] + '}',
list: settings['time_format'] + '{ - ' + settings['time_format'] + '}',
table: settings['time_format'] + '{ - ' + settings['time_format'] + '}'
},
axisFormat : settings['time_format'],
columnFormat: {
month: 'ddd', // Mon
week: 'ddd ' + settings['date_short'], // Mon 9/7
day: 'dddd ' + settings['date_short'], // Monday 9/7
list: settings['date_agenda'],
table: settings['date_agenda']
},
titleFormat: {
month: 'MMMM yyyy',
week: settings['dates_long'],
day: 'dddd ' + settings['date_long'],
list: settings['dates_long'],
table: settings['dates_long']
},
listSections: rcmail.env.listSections !== undefined ? rcmail.env.listSections : settings['agenda_sections'],
listRange: rcmail.env.listRange || settings['agenda_range'],
tableCols: ['handle', 'date', 'time', 'title', 'location'],
allDayText: rcmail.gettext('all-day', 'calendar'),
buttonText: {
day: rcmail.gettext('day', 'calendar'),
week: rcmail.gettext('week', 'calendar'),
month: rcmail.gettext('month', 'calendar'),
table: rcmail.gettext('agenda', 'calendar')
},
listTexts: {
until: rcmail.gettext('until', 'calendar'),
past: rcmail.gettext('pastevents', 'calendar'),
today: rcmail.gettext('today', 'calendar'),
tomorrow: rcmail.gettext('tomorrow', 'calendar'),
thisWeek: rcmail.gettext('thisweek', 'calendar'),
nextWeek: rcmail.gettext('nextweek', 'calendar'),
thisMonth: rcmail.gettext('thismonth', 'calendar'),
nextMonth: rcmail.gettext('nextmonth', 'calendar'),
future: rcmail.gettext('futureevents', 'calendar'),
week: rcmail.gettext('weekofyear', 'calendar')
},
loading: function(isLoading) {
rc_loading = rcmail.set_busy(isLoading, 'loading', rc_loading);
},
// event rendering
eventRender: function(event, element, view) {
if (view.name != 'month') {
var cont = element.find('div.fc-event-title');
if (event.location) {
cont.after('<div class="fc-event-location">@&nbsp;' + Q(event.location) + '</div>');
cont = cont.next();
}
if (event.description && showdesc) {
cont.after('<div class="fc-event-description">' + Q(event.description) + '</div>');
}
/* TODO: create icons black on white
if (event.recurrence)
element.find('div.fc-event-time').append('<i class="fc-icon-recurring"></i>');
if (event.alarms)
element.find('div.fc-event-time').append('<i class="fc-icon-alarms"></i>');
*/
}
if (view.name == 'table' && event.description && showdesc) {
var cols = element.children().css('border', 0).length;
element.after('<tr class="fc-event-row-secondary fc-event"><td colspan="'+cols+'" class="fc-event-description">' + Q(event.description) + '</td></tr>');
}
},
viewDisplay: function(view) {
// remove hard-coded hight and make contents visible
window.setTimeout(function(){
if (view.name == 'table') {
$('div.fc-list-content').css('overflow', 'visible').height('auto');
}
else {
$('div.fc-agenda-divider')
.next().css('overflow', 'visible').height('auto')
.children('div').css('overflow', 'visible').height('auto');
}
// adjust fixed height if vertical day slots
var h = $('table.fc-agenda-slots:visible').height() + $('table.fc-agenda-allday:visible').height() + 4;
if (h) $('table.fc-agenda-days td.fc-widget-content').children('div').height(h);
}, 20);
}
});
// activate settings form
$('#propdescription').change(function(){
showdesc = this.checked;
fc.fullCalendar('render');
});
});
diff --git a/plugins/kolab_addressbook/LICENSE b/plugins/kolab_addressbook/LICENSE
new file mode 100644
index 00000000..dba13ed2
--- /dev/null
+++ b/plugins/kolab_addressbook/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, 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
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state 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 program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index 29d533ed..576e76cc 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -1,590 +1,591 @@
<?php
/**
* Kolab address book 0.5
*
* Sample plugin to add a new address book source with data from Kolab storage
* It provides also a possibilities to manage contact folders
* (create/rename/delete/acl) directly in Addressbook UI.
*
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+
class kolab_addressbook extends rcube_plugin
{
public $task = 'mail|settings|addressbook|calendar';
private $folders;
private $sources;
private $rc;
private $ui;
const GLOBAL_FIRST = 0;
const PERSONAL_FIRST = 1;
const GLOBAL_ONLY = 2;
const PERSONAL_ONLY = 3;
/**
* Startup method of a Roundcube plugin
*/
public function init()
{
require_once(dirname(__FILE__) . '/lib/rcube_kolab_contacts.php');
$this->rc = rcmail::get_instance();
// load required plugin
$this->require_plugin('kolab_core');
// register hooks
$this->add_hook('addressbooks_list', array($this, 'address_sources'));
$this->add_hook('addressbook_get', array($this, 'get_address_book'));
$this->add_hook('config_get', array($this, 'config_get'));
if ($this->rc->task == 'addressbook') {
$this->add_texts('localization');
$this->add_hook('contact_form', array($this, 'contact_form'));
// Plugin actions
$this->register_action('plugin.book', array($this, 'book_actions'));
$this->register_action('plugin.book-save', array($this, 'book_save'));
// Load UI elements
if ($this->api->output->type == 'html') {
require_once($this->home . '/lib/kolab_addressbook_ui.php');
$this->ui = new kolab_addressbook_ui($this);
}
}
else if ($this->rc->task == 'settings') {
$this->add_texts('localization');
$this->add_hook('preferences_list', array($this, 'prefs_list'));
$this->add_hook('preferences_save', array($this, 'prefs_save'));
}
}
/**
* Handler for the addressbooks_list hook.
*
* This will add all instances of available Kolab-based address books
* to the list of address sources of Roundcube.
* This will also hide some addressbooks according to kolab_addressbook_prio setting.
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function address_sources($p)
{
// Load configuration
$this->load_config();
$abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio');
$undelete = $this->rc->config->get('undo_timeout');
// Disable all global address books
// Assumes that all non-kolab_addressbook sources are global
if ($abook_prio == self::PERSONAL_ONLY) {
$p['sources'] = array();
}
$sources = array();
$names = array();
foreach ($this->_list_sources() as $abook_id => $abook) {
$name = $origname = $abook->get_name();
// find folder prefix to truncate
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i].' &raquo; ') === 0) {
$length = strlen($names[$i].' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
break;
}
}
$names[] = $origname;
// register this address source
$sources[$abook_id] = array(
'id' => $abook_id,
'name' => $name,
'readonly' => $abook->readonly,
'editable' => $abook->editable,
'groups' => $abook->groups,
'undelete' => $abook->undelete && $undelete,
'realname' => rcube_charset_convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name
'class_name' => $abook->get_namespace(),
'kolab' => true,
);
}
// Add personal address sources to the list
if ($abook_prio == self::PERSONAL_FIRST) {
// $p['sources'] = array_merge($sources, $p['sources']);
// Don't use array_merge(), because if you have folders name
// that resolve to numeric identifier it will break output array keys
foreach ($p['sources'] as $idx => $value)
$sources[$idx] = $value;
$p['sources'] = $sources;
}
else {
// $p['sources'] = array_merge($p['sources'], $sources);
foreach ($sources as $idx => $value)
$p['sources'][$idx] = $value;
}
return $p;
}
/**
* Sets autocomplete_addressbooks option according to
* kolab_addressbook_prio setting extending list of address sources
* to be used for autocompletion.
*/
public function config_get($args)
{
if ($args['name'] != 'autocomplete_addressbooks') {
return $args;
}
// Load configuration
$this->load_config();
$abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio');
// here we cannot use rc->config->get()
$sources = $GLOBALS['CONFIG']['autocomplete_addressbooks'];
// Disable all global address books
// Assumes that all non-kolab_addressbook sources are global
if ($abook_prio == self::PERSONAL_ONLY) {
$sources = array();
}
if (!is_array($sources)) {
$sources = array();
}
$kolab_sources = array();
foreach ($this->_list_sources() as $abook_id => $abook) {
if (!in_array($abook_id, $sources))
$kolab_sources[] = $abook_id;
}
// Add personal address sources to the list
if (!empty($kolab_sources)) {
if ($abook_prio == self::PERSONAL_FIRST) {
$sources = array_merge($kolab_sources, $sources);
}
else {
$sources = array_merge($sources, $kolab_sources);
}
}
$args['result'] = $sources;
return $args;
}
/**
* Getter for the rcube_addressbook instance
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function get_address_book($p)
{
if ($p['id']) {
$this->_list_sources();
if ($this->sources[$p['id']]) {
$p['instance'] = $this->sources[$p['id']];
}
}
return $p;
}
private function _list_sources()
{
// already read sources
if (isset($this->sources))
return $this->sources;
$this->sources = array();
// Load configuration
$this->load_config();
$abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio');
// Personal address source(s) disabled?
if ($abook_prio == self::GLOBAL_ONLY) {
return $this->sources;
}
// get all folders that have "contact" type
$this->folders = rcube_kolab::get_folders('contact');
if (PEAR::isError($this->folders)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to list contact folders from Kolab server:" . $this->folders->getMessage()),
true, false);
}
else {
// convert to UTF8 and sort
$names = array();
foreach ($this->folders as $c_folder)
$names[$c_folder->name] = rcube_charset_convert($c_folder->name, 'UTF7-IMAP');
asort($names, SORT_LOCALE_STRING);
foreach ($names as $utf7name => $name) {
// create instance of rcube_contacts
$abook_id = rcube_kolab::folder_id($utf7name);
$abook = new rcube_kolab_contacts($utf7name);
$this->sources[$abook_id] = $abook;
}
}
return $this->sources;
}
/**
* Plugin hook called before rendering the contact form or detail view
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function contact_form($p)
{
// none of our business
if (!is_object($GLOBALS['CONTACTS']) || !is_a($GLOBALS['CONTACTS'], 'rcube_kolab_contacts'))
return $p;
// extend the list of contact fields to be displayed in the 'personal' section
if (is_array($p['form']['personal'])) {
$p['form']['contact']['content']['officelocation'] = array('size' => 40);
$p['form']['personal']['content']['initials'] = array('size' => 6);
$p['form']['personal']['content']['profession'] = array('size' => 40);
$p['form']['personal']['content']['children'] = array('size' => 40);
$p['form']['personal']['content']['pgppublickey'] = array('size' => 40);
$p['form']['personal']['content']['freebusyurl'] = array('size' => 40);
// re-order fields according to the coltypes list
$p['form']['contact']['content'] = $this->_sort_form_fields($p['form']['contact']['content']);
$p['form']['personal']['content'] = $this->_sort_form_fields($p['form']['personal']['content']);
/* define a separate section 'settings'
$p['form']['settings'] = array(
'name' => $this->gettext('settings'),
'content' => array(
'pgppublickey' => array('size' => 40, 'visible' => true),
'freebusyurl' => array('size' => 40, 'visible' => true),
)
);
*/
}
return $p;
}
private function _sort_form_fields($contents)
{
$block = array();
$contacts = reset($this->sources);
foreach ($contacts->coltypes as $col => $prop) {
if (isset($contents[$col]))
$block[$col] = $contents[$col];
}
return $block;
}
/**
* Handler for user preferences form (preferences_list hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_list($args)
{
if ($args['section'] != 'addressbook') {
return $args;
}
// Load configuration
$this->load_config();
// Load localization
$this->add_texts('localization');
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
if (!in_array('kolab_addressbook_prio', $dont_override)) {
$field_id = '_kolab_addressbook_prio';
$select = new html_select(array('name' => $field_id, 'id' => $field_id));
$select->add($this->gettext('globalfirst'), self::GLOBAL_FIRST);
$select->add($this->gettext('personalfirst'), self::PERSONAL_FIRST);
$select->add($this->gettext('globalonly'), self::GLOBAL_ONLY);
$select->add($this->gettext('personalonly'), self::PERSONAL_ONLY);
$args['blocks']['main']['options']['kolab_addressbook_prio'] = array(
'title' => html::label($field_id, Q($this->gettext('addressbookprio'))),
'content' => $select->show((int)$this->rc->config->get('kolab_addressbook_prio')),
);
}
return $args;
}
/**
* Handler for user preferences save (preferences_save hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_save($args)
{
if ($args['section'] != 'addressbook') {
return $args;
}
// Load configuration
$this->load_config();
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
if (!in_array('kolab_addressbook_prio', $dont_override)) {
$key = 'kolab_addressbook_prio';
$args['prefs'][$key] = (int) get_input_value('_'.$key, RCUBE_INPUT_POST);
}
return $args;
}
/**
* Handler for plugin actions
*/
public function book_actions()
{
$action = trim(get_input_value('_act', RCUBE_INPUT_GPC));
if ($action == 'create') {
$this->ui->book_edit();
}
else if ($action == 'edit') {
$this->ui->book_edit();
}
else if ($action == 'delete') {
$this->book_delete();
}
}
/**
* Handler for address book create/edit form submit
*/
public function book_save()
{
$folder = trim(get_input_value('_name', RCUBE_INPUT_POST, true, 'UTF7-IMAP'));
$oldfolder = trim(get_input_value('_oldname', RCUBE_INPUT_POST, true)); // UTF7-IMAP
$path = trim(get_input_value('_parent', RCUBE_INPUT_POST, true)); // UTF7-IMAP
$delimiter = $_SESSION['imap_delimiter'];
if (strlen($oldfolder)) {
$this->rc->imap_connect();
$options = $this->rc->imap->mailbox_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
$error = rcube_label('cannotbeempty');
}
else if (strlen($folder) > 128) {
$error = rcube_label('nametoolong');
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $delimiter) !== false) {
$error = rcube_label('forbiddencharacter') . " ($char)";
break;
}
}
}
if (!$error) {
if (!empty($options) && ($options['protected'] || $options['norename'])) {
$folder = $oldfolder;
}
else if (strlen($path)) {
$folder = $path . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
$this->rc->imap_init();
$folder = $this->rc->imap->mod_mailbox($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($path) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$this->rc->imap_connect();
$parent_opts = $this->rc->imap->mailbox_info($path);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts)))
) {
$error = rcube_label('parentnotwritable');
}
}
}
if (!$error) {
// update the folder name
if (strlen($oldfolder)) {
$type = 'update';
$plugin = $this->rc->plugins->exec_hook('addressbook_update', array(
'name' => $folder, 'oldname' => $oldfolder));
if (!$plugin['abort']) {
if ($oldfolder != $folder)
$result = rcube_kolab::folder_rename($oldfolder, $folder);
else
$result = true;
}
else {
$result = $plugin['result'];
}
}
// create new folder
else {
$type = 'create';
$plugin = $this->rc->plugins->exec_hook('addressbook_create', array('name' => $folder));
$folder = $plugin['name'];
if (!$plugin['abort']) {
$result = rcube_kolab::folder_create($folder, 'contact', false);
}
else {
$result = $plugin['result'];
}
}
}
if ($result) {
$kolab_folder = new rcube_kolab_contacts($folder);
// create display name for the folder (see self::address_sources())
if (strpos($folder, $delimiter)) {
$names = array();
foreach ($this->_list_sources() as $abook_id => $abook) {
$realname = $abook->get_realname();
// The list can be not updated yet, handle old folder name
if ($type == 'update' && $realname == $oldfolder) {
$abook = $kolab_folder;
$realname = $folder;
}
$name = $origname = $abook->get_name();
// find folder prefix to truncate
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i].' &raquo; ') === 0) {
$length = strlen($names[$i].' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
break;
}
}
$names[] = $origname;
if ($realname == $folder) {
break;
}
}
}
else {
$name = $kolab_folder->get_name();
}
$this->rc->output->show_message('kolab_addressbook.book'.$type.'d', 'confirmation');
$this->rc->output->command('set_env', 'delimiter', $delimiter);
$this->rc->output->command('book_update', array(
'id' => rcube_kolab::folder_id($folder),
'name' => $name,
'readonly' => false,
'editable' => true,
'groups' => true,
'realname' => rcube_charset_convert($folder, 'UTF7-IMAP'), // IMAP folder name
'class_name' => $kolab_folder->get_namespace(),
'kolab' => true,
), rcube_kolab::folder_id($oldfolder));
$this->rc->output->send('iframe');
}
if (!$error)
$error = $plugin['message'] ? $plugin['message'] : 'kolab_addressbook.book'.$type.'error';
$this->rc->output->show_message($error, 'error');
// display the form again
$this->ui->book_edit();
}
/**
* Handler for address book delete action (AJAX)
*/
private function book_delete()
{
$folder = trim(get_input_value('_source', RCUBE_INPUT_GPC, true, 'UTF7-IMAP'));
if (rcube_kolab::folder_delete($folder)) {
$this->rc->output->show_message('kolab_addressbook.bookdeleted', 'confirmation');
$this->rc->output->set_env('pagecount', 0);
$this->rc->output->command('set_rowcount', rcmail_get_rowcount_text(new rcube_result_set()));
$this->rc->output->command('list_contacts_clear');
$this->rc->output->command('book_delete_done', rcube_kolab::folder_id($folder));
}
else {
$this->rc->output->show_message('kolab_addressbook.bookdeleteerror', 'error');
}
$this->rc->output->send();
}
}
diff --git a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
index 3f808b4b..d154a0a7 100644
--- a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
+++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
@@ -1,281 +1,281 @@
<?php
/**
* Kolab address book UI
*
* @author Aleksander Machniak <machniak@kolabsys.com>
*
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_addressbook_ui
{
private $plugin;
private $rc;
/**
* Class constructor
*
* @param kolab_addressbook $plugin Plugin object
*/
public function __construct($plugin)
{
$this->rc = rcmail::get_instance();
$this->plugin = $plugin;
$this->init_ui();
}
/**
* Adds folders management functionality to Addressbook UI
*/
private function init_ui()
{
if (!empty($this->rc->action) && !preg_match('/^plugin\.book/', $this->rc->action)) {
return;
}
// Include script
$this->plugin->include_script('kolab_addressbook.js');
if (empty($this->rc->action)) {
// Include stylesheet (for directorylist)
$this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css');
// Add actions on address books
$options = array('book-create', 'book-edit', 'book-delete');
$idx = 0;
foreach ($options as $command) {
$content = html::tag('li', $idx ? null : array('class' => 'separator_above'),
$this->plugin->api->output->button(array(
'label' => 'kolab_addressbook.'.str_replace('-', '', $command),
'domain' => $this->ID,
'classact' => 'active',
'command' => $command
)));
$this->plugin->api->add_content($content, 'groupoptions');
$idx++;
}
// Link to Settings/Folders
$content = html::tag('li', array('class' => 'separator_above'),
$this->plugin->api->output->button(array(
'label' => 'managefolders',
'type' => 'link',
'classact' => 'active',
'command' => 'folders',
'task' => 'settings',
)));
$this->plugin->api->add_content($content, 'groupoptions');
$this->rc->output->add_label('kolab_addressbook.bookdeleteconfirm',
'kolab_addressbook.bookdeleting');
}
// book create/edit form
else {
$this->rc->output->add_label('kolab_addressbook.nobooknamewarning',
'kolab_addressbook.booksaving');
}
}
/**
* Handler for address book create/edit action
*/
public function book_edit()
{
$this->rc->output->add_handler('bookdetails', array($this, 'book_form'));
$this->rc->output->send('kolab_addressbook.bookedit');
}
/**
* Handler for 'bookdetails' object returning form content for book create/edit
*
* @param array $attr Object attributes
*
* @return string HTML output
*/
public function book_form($attrib)
{
$action = trim(get_input_value('_act', RCUBE_INPUT_GPC));
$folder = trim(get_input_value('_source', RCUBE_INPUT_GPC, true)); // UTF8
$hidden_fields[] = array('name' => '_source', 'value' => $folder);
$folder = rcube_charset_convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP');
$delim = $_SESSION['imap_delimiter'];
if ($this->rc->action == 'plugin.book-save') {
// save error
$name = trim(get_input_value('_name', RCUBE_INPUT_GPC, true)); // UTF8
$old = trim(get_input_value('_oldname', RCUBE_INPUT_GPC, true)); // UTF7-IMAP
$path_imap = trim(get_input_value('_parent', RCUBE_INPUT_GPC, true)); // UTF7-IMAP
$hidden_fields[] = array('name' => '_oldname', 'value' => $old);
$folder = $old;
}
else if ($action == 'edit') {
$path_imap = explode($delim, $folder);
$name = rcube_charset_convert(array_pop($path_imap), 'UTF7-IMAP');
$path_imap = implode($path_imap, $delim);
}
else { // create
$path_imap = $folder;
$name = '';
$folder = '';
}
// Store old name, get folder options
if (strlen($folder)) {
$hidden_fields[] = array('name' => '_oldname', 'value' => $folder);
$this->rc->imap_connect();
$options = $this->rc->imap->mailbox_info($folder);
}
$form = array();
// General tab
$form['props'] = array(
'name' => $this->rc->gettext('properties'),
);
if (!empty($options) && ($options['norename'] || $options['protected'])) {
$foldername = Q(str_replace($delimiter, ' &raquo; ', rcube_kolab::object_name($folder)));
}
else {
$foldername = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30));
$foldername = $foldername->show($name);
}
$form['props']['fieldsets']['location'] = array(
'name' => $this->rc->gettext('location'),
'content' => array(
'name' => array(
'label' => $this->plugin->gettext('bookname'),
'value' => $foldername,
),
),
);
if (!empty($options) && ($options['norename'] || $options['protected'])) {
// prevent user from moving folder
$hidden_fields[] = array('name' => '_parent', 'value' => $path_imap);
}
else {
$select = rcube_kolab::folder_selector('contact', array('name' => '_parent'), $folder);
$form['props']['fieldsets']['location']['content']['path'] = array(
'label' => $this->plugin->gettext('parentbook'),
'value' => $select->show(strlen($folder) ? $path_imap : ''),
);
}
// Allow plugins to modify address book form content (e.g. with ACL form)
$plugin = $this->rc->plugins->exec_hook('addressbook_form',
array('form' => $form, 'options' => $options, 'name' => $folder));
$form = $plugin['form'];
// Set form tags and hidden fields
list($form_start, $form_end) = $this->get_form_tags($attrib, 'plugin.book-save', null, $hidden_fields);
unset($attrib['form']);
// return the complete edit form as table
$out = "$form_start\n";
// Create form output
foreach ($form as $tab) {
if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) {
$content = '';
foreach ($tab['fieldsets'] as $fieldset) {
$subcontent = $this->get_form_part($fieldset);
if ($subcontent) {
$content .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $subcontent) ."\n";
}
}
}
else {
$content = $this->get_form_part($tab);
}
if ($content) {
$out .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n";
}
}
$out .= "\n$form_end";
return $out;
}
private function get_form_part($form)
{
$content = '';
if (is_array($form['content']) && !empty($form['content'])) {
$table = new html_table(array('cols' => 2));
foreach ($form['content'] as $col => $colprop) {
$colprop['id'] = '_'.$col;
$label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
$table->add('title', sprintf('<label for="%s">%s</label>', $colprop['id'], Q($label)));
$table->add(null, $colprop['value']);
}
$content = $table->show();
}
else {
$content = $form['content'];
}
return $content;
}
private function get_form_tags($attrib, $action, $id = null, $hidden = null)
{
$form_start = $form_end = '';
$request_key = $action . (isset($id) ? '.'.$id : '');
$form_start = $this->rc->output->request_form(array(
'name' => 'form',
'method' => 'post',
'task' => $this->rc->task,
'action' => $action,
'request' => $request_key,
'noclose' => true,
) + $attrib);
if (is_array($hidden)) {
foreach ($hidden as $field) {
$hiddenfield = new html_hiddenfield($field);
$form_start .= $hiddenfield->show();
}
}
$form_end = !strlen($attrib['form']) ? '</form>' : '';
$EDIT_FORM = !empty($attrib['form']) ? $attrib['form'] : 'form';
$this->rc->output->add_gui_object('editform', $EDIT_FORM);
return array($form_start, $form_end);
}
}
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 3917543c..0ec22ac3 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -1,1201 +1,1217 @@
<?php
-
/**
* Backend class for a custom address book
*
* This part of the Roundcube+Kolab integration and connects the
* rcube_addressbook interface with the rcube_kolab wrapper for Kolab_Storage
*
- * @author Thomas Bruederli
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
* @see rcube_addressbook
*/
class rcube_kolab_contacts extends rcube_addressbook
{
public $primary_key = 'ID';
public $readonly = true;
public $editable = false;
public $undelete = true;
public $groups = true;
public $coltypes = array(
'name' => array('limit' => 1),
'firstname' => array('limit' => 1),
'surname' => array('limit' => 1),
'middlename' => array('limit' => 1),
'prefix' => array('limit' => 1),
'suffix' => array('limit' => 1),
'nickname' => array('limit' => 1),
'jobtitle' => array('limit' => 1),
'organization' => array('limit' => 1),
'department' => array('limit' => 1),
'email' => array('subtypes' => null),
'phone' => array(),
'address' => array('limit' => 2, 'subtypes' => array('home','business')),
'officelocation' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1,
'label' => 'kolab_addressbook.officelocation', 'category' => 'main'),
'website' => array('limit' => 1, 'subtypes' => null),
'im' => array('limit' => 1, 'subtypes' => null),
'gender' => array('limit' => 1),
'initials' => array('type' => 'text', 'size' => 6, 'maxlength' => 10, 'limit' => 1,
'label' => 'kolab_addressbook.initials', 'category' => 'personal'),
'birthday' => array('limit' => 1),
'anniversary' => array('limit' => 1),
'profession' => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => 1,
'label' => 'kolab_addressbook.profession', 'category' => 'personal'),
'manager' => array('limit' => 1),
'assistant' => array('limit' => 1),
'spouse' => array('limit' => 1),
'children' => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => 1,
'label' => 'kolab_addressbook.children', 'category' => 'personal'),
'pgppublickey' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.pgppublickey'),
'freebusyurl' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.freebusyurl'),
'notes' => array(),
'photo' => array(),
// TODO: define more Kolab-specific fields such as: language, latitude, longitude
);
/**
* vCard additional fields mapping
*/
public $vcard_map = array(
'profession' => 'X-PROFESSION',
'officelocation' => 'X-OFFICE-LOCATION',
'initials' => 'X-INITIALS',
'children' => 'X-CHILDREN',
'freebusyurl' => 'X-FREEBUSY-URL',
'pgppublickey' => 'KEY',
);
private $gid;
private $storagefolder;
private $contactstorage;
private $liststorage;
private $contacts;
private $distlists;
private $groupmembers;
private $id2uid;
private $filter;
private $result;
private $namespace;
private $imap_folder = 'INBOX/Contacts';
private $gender_map = array(0 => 'male', 1 => 'female');
private $phonetypemap = array('home' => 'home1', 'work' => 'business1', 'work2' => 'business2', 'workfax' => 'businessfax');
private $addresstypemap = array('work' => 'business');
private $fieldmap = array(
// kolab => roundcube
'full-name' => 'name',
'given-name' => 'firstname',
'middle-names' => 'middlename',
'last-name' => 'surname',
'prefix' => 'prefix',
'suffix' => 'suffix',
'nick-name' => 'nickname',
'organization' => 'organization',
'department' => 'department',
'job-title' => 'jobtitle',
'initials' => 'initials',
'birthday' => 'birthday',
'anniversary' => 'anniversary',
'im-address' => 'im',
'web-page' => 'website',
'office-location' => 'officelocation',
'profession' => 'profession',
'manager-name' => 'manager',
'assistant' => 'assistant',
'spouse-name' => 'spouse',
'children' => 'children',
'body' => 'notes',
'pgp-publickey' => 'pgppublickey',
'free-busy-url' => 'freebusyurl',
'gender' => 'gender',
);
public function __construct($imap_folder = null)
{
if ($imap_folder)
$this->imap_folder = $imap_folder;
// extend coltypes configuration
$format = rcube_kolab::get_format('contact');
$this->coltypes['phone']['subtypes'] = $format->_phone_types;
$this->coltypes['address']['subtypes'] = $format->_address_types;
// set localized labels for proprietary cols
foreach ($this->coltypes as $col => $prop) {
if (is_string($prop['label']))
$this->coltypes[$col]['label'] = rcube_label($prop['label']);
}
// fetch objects from the given IMAP folder
$this->storagefolder = rcube_kolab::get_folder($this->imap_folder);
$this->ready = !PEAR::isError($this->storagefolder);
// Set readonly and editable flags according to folder permissions
if ($this->ready) {
if ($this->get_owner() == $_SESSION['username']) {
$this->editable = true;
$this->readonly = false;
}
else {
$rights = $this->storagefolder->getMyRights();
if (!PEAR::isError($rights)) {
if (strpos($rights, 'i') !== false)
$this->readonly = false;
if (strpos($rights, 'a') !== false || strpos($rights, 'x') !== false)
$this->editable = true;
}
}
}
}
/**
* Getter for the address book name to be displayed
*
* @return string Name of this address book
*/
public function get_name()
{
$folder = rcube_kolab::object_name($this->imap_folder, $this->namespace);
return $folder;
}
/**
* Getter for the IMAP folder name
*
* @return string Name of the IMAP folder
*/
public function get_realname()
{
return $this->imap_folder;
}
/**
* Getter for the IMAP folder owner
*
* @return string Name of the folder owner
*/
public function get_owner()
{
return $this->storagefolder->getOwner();
}
/**
* Getter for the name of the namespace to which the IMAP folder belongs
*
* @return string Name of the namespace (personal, other, shared)
*/
public function get_namespace()
{
if ($this->namespace === null) {
$this->namespace = rcube_kolab::folder_namespace($this->imap_folder);
}
return $this->namespace;
}
/**
* Setter for the current group
*/
public function set_group($gid)
{
$this->gid = $gid;
}
/**
* Save a search string for future listings
*
* @param mixed Search params to use in listing method, obtained by get_search_set()
*/
public function set_search_set($filter)
{
$this->filter = $filter;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
public function get_search_set()
{
return $this->filter;
}
/**
* Reset saved results and search parameters
*/
public function reset()
{
$this->result = null;
$this->filter = null;
}
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null)
{
$this->_fetch_groups();
$groups = array();
foreach ((array)$this->distlists as $group) {
if (!$search || strstr(strtolower($group['last-name']), strtolower($search)))
$groups[$group['last-name']] = array('ID' => $group['ID'], 'name' => $group['last-name']);
}
// sort groups
ksort($groups, SORT_LOCALE_STRING);
return array_values($groups);
}
/**
* List the current set of contact records
*
* @param array List of cols to show
* @param int Only return this number of records, use negative values for tail
* @return array Indexed list of contact records, each a hash array
*/
public function list_records($cols=null, $subset=0)
{
$this->result = $this->count();
// list member of the selected group
if ($this->gid) {
$seen = array();
$this->result->count = 0;
foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
// skip member that don't match the search filter
if (is_array($this->filter['ids']) && array_search($member['ID'], $this->filter['ids']) === false)
continue;
if ($this->contacts[$member['ID']] && !$seen[$member['ID']]++)
$this->result->count++;
}
$ids = array_keys($seen);
}
else
$ids = is_array($this->filter['ids']) ? $this->filter['ids'] : array_keys($this->contacts);
// sort data arrays according to desired list sorting
if ($count = count($ids)) {
uasort($this->contacts, array($this, '_sort_contacts_comp'));
// get sorted IDs
if ($count != count($this->contacts))
$ids = array_values(array_intersect(array_keys($this->contacts), $ids));
else
$ids = array_keys($this->contacts);
}
// fill contact data into the current result set
$start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
$last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, $count);
for ($i = $start_row; $i < $last_row; $i++) {
if ($id = $ids[$i])
$this->result->add($this->contacts[$id]);
}
return $this->result;
}
/**
* Search records
*
* @param mixed $fields The field name of array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param boolean $strict True for strict (=), False for partial (LIKE) matching
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return object rcube_result_set List of contact records and 'count' value
*/
public function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
{
$this->_fetch_contacts();
// search by ID
if ($fields == $this->primary_key) {
$ids = !is_array($value) ? explode(',', $value) : $value;
$result = new rcube_result_set();
foreach ($ids as $id) {
if ($rec = $this->get_record($id, true)) {
$result->add($rec);
$result->count++;
}
}
return $result;
}
else if ($fields == '*') {
$fields = array_keys($this->coltypes);
}
if (!is_array($fields))
$fields = array($fields);
if (!is_array($required) && !empty($required))
$required = array($required);
// advanced search
if (is_array($value)) {
$advanced = true;
$value = array_map('mb_strtolower', $value);
}
else
$value = mb_strtolower($value);
$scount = count($fields);
// build key name regexp
$regexp = '/^(' . implode($fields, '|') . ')(?:.*)$/';
// save searching conditions
$this->filter = array('fields' => $fields, 'value' => $value, 'strict' => $strict, 'ids' => array());
// search be iterating over all records in memory
foreach ($this->contacts as $id => $contact) {
// check if current contact has required values, otherwise skip it
if ($required) {
foreach ($required as $f)
if (empty($contact[$f]))
continue 2;
}
$found = array();
foreach (preg_grep($regexp, array_keys($contact)) as $col) {
if ($advanced) {
$pos = strpos($col, ':');
$colname = $pos ? substr($col, 0, $pos) : $col;
$search = $value[array_search($colname, $fields)];
}
else {
$search = $value;
}
$s_len = strlen($search);
foreach ((array)$contact[$col] as $val) {
// composite field, e.g. address
if (is_array($val)) {
$val = implode($val);
}
$val = mb_strtolower($val);
if (($strict && $val == $search)
|| (!$strict && $s_len && strpos($val, $search) !== false)
) {
if (!$advanced) {
$this->filter['ids'][] = $id;
break 2;
}
else {
$found[$colname] = true;
}
}
}
}
if (count($found) >= $scount) // && $advanced
$this->filter['ids'][] = $id;
}
// list records (now limited by $this->filter)
return $this->list_records();
}
/**
* Refresh saved search results after data has changed
*/
public function refresh_search()
{
if ($this->filter)
$this->search($this->filter['fields'], $this->filter['value'], $this->filter['strict']);
return $this->get_search_set();
}
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result set with values for 'count' and 'first'
*/
public function count()
{
$this->_fetch_contacts();
$this->_fetch_groups();
$count = $this->gid ? count($this->distlists[$this->gid]['member']) : (is_array($this->filter['ids']) ? count($this->filter['ids']) : count($this->contacts));
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Return the last result set
*
* @return rcube_result_set Current result set or NULL if nothing selected yet
*/
public function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed record identifier(s)
* @param boolean True to return record as associative array, otherwise a result set is returned
* @return mixed Result object with all record fields or False if not found
*/
public function get_record($id, $assoc=false)
{
$this->_fetch_contacts();
if ($this->contacts[$id]) {
$this->result = new rcube_result_set(1);
$this->result->add($this->contacts[$id]);
return $assoc ? $this->contacts[$id] : $this->result;
}
return false;
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
* @return array List of assigned groups as ID=>Name pairs
*/
public function get_record_groups($id)
{
$out = array();
$this->_fetch_groups();
foreach ((array)$this->groupmembers[$id] as $gid) {
if ($group = $this->distlists[$gid])
$out[$gid] = $group['last-name'];
}
return $out;
}
/**
* Create a new contact record
*
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @param boolean True to check for duplicates first
* @return mixed The created record ID on success, False on error
*/
public function insert($save_data, $check=false)
{
if (!is_array($save_data))
return false;
$insert_id = $existing = false;
// check for existing records by e-mail comparison
if ($check) {
foreach ($this->get_col_values('email', $save_data, true) as $email) {
if (($res = $this->search('email', $email, true, false)) && $res->count) {
$existing = true;
break;
}
}
}
if (!$existing) {
$this->_connect();
// generate new Kolab contact item
$object = $this->_from_rcube_contact($save_data);
$object['uid'] = $this->contactstorage->generateUID();
$saved = $this->contactstorage->save($object);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
$contact = $this->_to_rcube_contact($object);
$id = $contact['ID'];
$this->contacts[$id] = $contact;
$this->id2uid[$id] = $object['uid'];
$insert_id = $id;
}
}
return $insert_id;
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @return boolean True on success, False on error
*/
public function update($id, $save_data)
{
$updated = false;
$this->_fetch_contacts();
if ($this->contacts[$id] && ($uid = $this->id2uid[$id])) {
$old = $this->contactstorage->getObject($uid);
$object = array_merge($old, $this->_from_rcube_contact($save_data));
$saved = $this->contactstorage->save($object, $uid);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
$this->contacts[$id] = $this->_to_rcube_contact($object);
$updated = true;
}
}
return $updated;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
* @param boolean Remove record(s) irreversible (mark as deleted otherwise)
*
* @return int Number of records deleted
*/
public function delete($ids, $force=true)
{
$this->_fetch_contacts();
$this->_fetch_groups();
if (!is_array($ids))
$ids = explode(',', $ids);
$count = 0;
$imap_uids = array();
foreach ($ids as $id) {
if ($uid = $this->id2uid[$id]) {
$imap_uid = $this->contactstorage->_getStorageId($uid);
$deleted = $this->contactstorage->delete($uid, $force);
if (PEAR::isError($deleted)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting a contact object from the Kolab server:" . $deleted->getMessage()),
true, false);
}
else {
// remove from distribution lists
foreach ((array)$this->groupmembers[$id] as $gid)
$this->remove_from_group($gid, $id);
$imap_uids[$id] = $imap_uid;
// clear internal cache
unset($this->contacts[$id], $this->id2uid[$id], $this->groupmembers[$id]);
$count++;
}
}
}
// store IMAP uids for undelete()
if (!$force)
$_SESSION['kolab_delete_uids'] = $imap_uids;
return $count;
}
/**
* Undelete one or more contact records.
* Only possible just after delete (see 2nd argument of delete() method).
*
* @param array Record identifiers
*
* @return int Number of records restored
*/
public function undelete($ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
$count = 0;
$uids = array();
$imap_uids = $_SESSION['kolab_delete_uids'];
// convert contact IDs into IMAP UIDs
foreach ($ids as $id)
if ($uid = $imap_uids[$id])
$uids[] = $uid;
if (!empty($uids)) {
$session = &Horde_Kolab_Session::singleton();
$imap = &$session->getImap();
if (is_object($imap) && is_a($imap, 'PEAR_Error')) {
$error = $imap;
}
else {
$result = $imap->select($this->imap_folder);
if (is_object($result) && is_a($result, 'PEAR_Error')) {
$error = $result;
}
else {
$result = $imap->undeleteMessages(implode(',', $uids));
if (is_object($result) && is_a($result, 'PEAR_Error')) {
$error = $result;
}
else {
$this->_connect();
$this->contactstorage->synchronize();
}
}
}
if ($error) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error undeleting a contact object(s) from the Kolab server:" . $error->getMessage()),
true, false);
}
$rcmail = rcmail::get_instance();
$rcmail->session->remove('kolab_delete_uids');
}
return count($uids);
}
/**
* Remove all records from the database
*/
public function delete_all()
{
$this->_connect();
if (!PEAR::isError($this->contactstorage->deleteAll())) {
$this->contacts = array();
$this->id2uid = array();
$this->result = null;
}
}
/**
* Close connection to source
* Called on script shutdown
*/
public function close()
{
}
/**
* Create a contact group with the given name
*
* @param string The group name
* @return mixed False on error, array with record props in success
*/
function create_group($name)
{
$this->_fetch_groups();
$result = false;
$list = array(
'uid' => $this->liststorage->generateUID(),
'last-name' => $name,
'member' => array(),
);
$saved = $this->liststorage->save($list);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
true, false);
return false;
}
else {
$id = md5($list['uid']);
$this->distlists[$record['ID']] = $list;
$result = array('id' => $id, 'name' => $name);
}
return $result;
}
/**
* Delete the given group and all linked group members
*
* @param string Group identifier
* @return boolean True on success, false if no data was changed
*/
function delete_group($gid)
{
$this->_fetch_groups();
$result = false;
if ($list = $this->distlists[$gid])
$deleted = $this->liststorage->delete($list['uid']);
if (PEAR::isError($deleted)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting distribution-list object from the Kolab server:" . $deleted->getMessage()),
true, false);
}
else
$result = true;
return $result;
}
/**
* Rename a specific contact group
*
* @param string Group identifier
* @param string New name to set for this group
* @return boolean New name on success, false if no data was changed
*/
function rename_group($gid, $newname)
{
$this->_fetch_groups();
$list = $this->distlists[$gid];
if ($newname != $list['last-name']) {
$list['last-name'] = $newname;
$saved = $this->liststorage->save($list, $list['uid']);
}
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
true, false);
return false;
}
return $newname;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be added
* @return int Number of contacts added
*/
function add_to_group($gid, $ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
$added = 0;
$exists = array();
$this->_fetch_groups();
$this->_fetch_contacts();
$list = $this->distlists[$gid];
foreach ((array)$list['member'] as $i => $member)
$exists[] = $member['ID'];
// substract existing assignments from list
$ids = array_diff($ids, $exists);
foreach ($ids as $contact_id) {
if ($uid = $this->id2uid[$contact_id]) {
$contact = $this->contacts[$contact_id];
foreach ($this->get_col_values('email', $contact, true) as $email) {
$list['member'][] = array(
'uid' => $uid,
'display-name' => $contact['name'],
'smtp-address' => $email,
);
}
$this->groupmembers[$contact_id][] = $gid;
$added++;
}
}
if ($added)
$saved = $this->liststorage->save($list, $list['uid']);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list to Kolab server:" . $saved->getMessage()),
true, false);
$added = false;
}
else {
$this->distlists[$gid] = $list;
}
return $added;
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be removed
* @return int Number of deleted group members
*/
function remove_from_group($gid, $ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
$this->_fetch_groups();
if (!($list = $this->distlists[$gid]))
return false;
$new_member = array();
foreach ((array)$list['member'] as $member) {
if (!in_array($member['ID'], $ids))
$new_member[] = $member;
}
// write distribution list back to server
$list['member'] = $new_member;
$saved = $this->liststorage->save($list, $list['uid']);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
// remove group assigments in local cache
foreach ($ids as $id) {
$j = array_search($gid, $this->groupmembers[$id]);
unset($this->groupmembers[$id][$j]);
}
$this->distlists[$gid] = $list;
return true;
}
return false;
}
/**
* Check the given data before saving.
* If input not valid, the message to display can be fetched using get_error()
*
* @param array Associative array with contact data to save
*
* @return boolean True if input is valid, False if not.
*/
public function validate($save_data)
{
// validate e-mail addresses
$valid = parent::validate($save_data);
// require at least one e-mail address (syntax check is already done)
if ($valid) {
if (!strlen($save_data['name'])
&& !array_filter($this->get_col_values('email', $save_data, true))
) {
$this->set_error('warning', 'kolab_addressbook.noemailnamewarning');
$valid = false;
}
}
return $valid;
}
/**
* Establishes a connection to the Kolab_Data object for accessing contact data
*/
private function _connect()
{
if (!isset($this->contactstorage)) {
$this->contactstorage = $this->storagefolder->getData(null);
}
}
/**
* Establishes a connection to the Kolab_Data object for accessing groups data
*/
private function _connect_groups()
{
if (!isset($this->liststorage)) {
$this->liststorage = $this->storagefolder->getData('distributionlist');
}
}
/**
* Simply fetch all records and store them in private member vars
*/
private function _fetch_contacts()
{
if (!isset($this->contacts)) {
$this->_connect();
// read contacts
$this->contacts = $this->id2uid = array();
foreach ((array)$this->contactstorage->getObjects() as $record) {
// Because of a bug, sometimes group records are returned
if ($record['__type'] == 'Group')
continue;
$contact = $this->_to_rcube_contact($record);
$id = $contact['ID'];
$this->contacts[$id] = $contact;
$this->id2uid[$id] = $record['uid'];
}
}
}
/**
* Callback function for sorting contacts
*/
private function _sort_contacts_comp($a, $b)
{
$a_name = $a['name'];
$b_name = $b['name'];
if (!$a_name) {
$a_name = join(' ', array_filter(array($a['prefix'], $a['firstname'],
$a['middlename'], $a['surname'], $a['suffix'])));
if (!$a_name) {
$a_name = is_array($a['email']) ? $a['email'][0] : $a['email'];
}
}
if (!$b_name) {
$b_name = join(' ', array_filter(array($b['prefix'], $b['firstname'],
$b['middlename'], $b['surname'], $b['suffix'])));
if (!$b_name) {
$b_name = is_array($b['email']) ? $b['email'][0] : $b['email'];
}
}
// return strcasecmp($a_name, $b_name);
// make sorting unicode-safe and locale-dependent
if ($a_name == $b_name)
return 0;
$arr = array($a_name, $b_name);
sort($arr, SORT_LOCALE_STRING);
return $a_name == $arr[0] ? -1 : 1;
}
/**
* Read distribution-lists AKA groups from server
*/
private function _fetch_groups()
{
if (!isset($this->distlists)) {
$this->_connect_groups();
$this->distlists = $this->groupmembers = array();
foreach ((array)$this->liststorage->getObjects() as $record) {
// FIXME: folders without any distribution-list objects return contacts instead ?!
if ($record['__type'] != 'Group')
continue;
$record['ID'] = md5($record['uid']);
foreach ((array)$record['member'] as $i => $member) {
$mid = md5($member['uid']);
$record['member'][$i]['ID'] = $mid;
$this->groupmembers[$mid][] = $record['ID'];
}
$this->distlists[$record['ID']] = $record;
}
}
}
/**
* Map fields from internal Kolab_Format to Roundcube contact format
*/
private function _to_rcube_contact($record)
{
$out = array(
'ID' => md5($record['uid']),
'email' => array(),
'phone' => array(),
);
foreach ($this->fieldmap as $kolab => $rcube) {
if (strlen($record[$kolab]))
$out[$rcube] = $record[$kolab];
}
if (isset($record['gender']))
$out['gender'] = $this->gender_map[$record['gender']];
foreach ((array)$record['email'] as $i => $email)
$out['email'][] = $email['smtp-address'];
if (!$record['email'] && $record['emails'])
$out['email'] = preg_split('/,\s*/', $record['emails']);
foreach ((array)$record['phone'] as $i => $phone)
$out['phone:'.$phone['type']][] = $phone['number'];
if (is_array($record['address'])) {
foreach ($record['address'] as $i => $adr) {
$key = 'address:' . $adr['type'];
$out[$key][] = array(
'street' => $adr['street'],
'locality' => $adr['locality'],
'zipcode' => $adr['postal-code'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
}
// photo is stored as separate attachment
if ($record['picture'] && ($att = $record['_attachments'][$record['picture']])) {
$out['photo'] = $att['content'] ? $att['content'] : $this->contactstorage->getAttachment($att['key']);
}
// remove empty fields
return array_filter($out);
}
/**
* Map fields from Roundcube format to internal Kolab_Format
*/
private function _from_rcube_contact($contact)
{
$object = array();
foreach (array_flip($this->fieldmap) as $rcube => $kolab) {
if (isset($contact[$rcube]))
$object[$kolab] = is_array($contact[$rcube]) ? $contact[$rcube][0] : $contact[$rcube];
else if ($values = $this->get_col_values($rcube, $contact, true))
$object[$kolab] = is_array($values) ? $values[0] : $values;
}
// format dates
if ($object['birthday'] && ($date = @strtotime($object['birthday'])))
$object['birthday'] = date('Y-m-d', $date);
if ($object['anniversary'] && ($date = @strtotime($object['anniversary'])))
$object['anniversary'] = date('Y-m-d', $date);
$gendermap = array_flip($this->gender_map);
if (isset($object['gender']))
$object['gender'] = $gendermap[$object['gender']];
$emails = $this->get_col_values('email', $contact, true);
$object['emails'] = join(', ', array_filter($emails));
// overwrite 'email' field
$object['email'] = null;
foreach ($this->get_col_values('phone', $contact) as $type => $values) {
if ($this->phonetypemap[$type])
$type = $this->phonetypemap[$type];
foreach ((array)$values as $phone) {
if (!empty($phone)) {
$object['phone-' . $type] = $phone;
$object['phone'][] = array('number' => $phone, 'type' => $type);
}
}
}
$object['address'] = array();
foreach ($this->get_col_values('address', $contact) as $type => $values) {
if ($this->addresstypemap[$type])
$type = $this->addresstypemap[$type];
$updated = false;
$basekey = 'addr-' . $type . '-';
foreach ((array)$values as $adr) {
// skip empty address
$adr = array_filter($adr);
if (empty($adr))
continue;
// switch type if slot is already taken
if (isset($object[$basekey . 'type'])) {
$type = $type == 'home' ? 'business' : 'home';
$basekey = 'addr-' . $type . '-';
}
if (!isset($object[$basekey . 'type'])) {
$object[$basekey . 'type'] = $type;
$object[$basekey . 'street'] = $adr['street'];
$object[$basekey . 'locality'] = $adr['locality'];
$object[$basekey . 'postal-code'] = $adr['zipcode'];
$object[$basekey . 'region'] = $adr['region'];
$object[$basekey . 'country'] = $adr['country'];
// Update existing address entry of this type
foreach($object['address'] as $index => $address) {
if ($address['type'] == $type) {
$object['address'][$index] = $new_address;
$updated = true;
}
}
}
if (!$updated) {
$object['address'][] = array(
'type' => $type,
'street' => $adr['street'],
'locality' => $adr['locality'],
'postal-code' => $adr['zipcode'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
}
}
// save new photo as attachment
if ($contact['photo']) {
$attkey = 'photo.attachment';
$object['_attachments'][$attkey] = array(
'type' => rc_image_content_type($contact['photo']),
'content' => preg_match('![^a-z0-9/=+-]!i', $contact['photo']) ? $contact['photo'] : base64_decode($contact['photo']),
);
$object['picture'] = $attkey;
}
return $object;
}
}
diff --git a/plugins/kolab_auth/LICENSE b/plugins/kolab_auth/LICENSE
new file mode 100644
index 00000000..dba13ed2
--- /dev/null
+++ b/plugins/kolab_auth/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, 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
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state 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 program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/plugins/kolab_auth/kolab_auth.php b/plugins/kolab_auth/kolab_auth.php
index d7e07bd3..f1114358 100644
--- a/plugins/kolab_auth/kolab_auth.php
+++ b/plugins/kolab_auth/kolab_auth.php
@@ -1,397 +1,397 @@
<?php
/**
* Kolab Authentication (based on ldap_authentication plugin)
*
* Authenticates on LDAP server, finds canonized authentication ID for IMAP
* and for new users creates identity based on LDAP information.
*
* Supports impersonate feature (login as another user). To use this feature
* imap_auth_type/smtp_auth_type must be set to DIGEST-MD5 or PLAIN.
*
* @version 0.1
* @author Aleksander Machniak <machniak@kolabsys.com>
*
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_auth extends rcube_plugin
{
private $ldap;
private $data = array();
public function init()
{
$rcmail = rcmail::get_instance();
$this->add_hook('authenticate', array($this, 'authenticate'));
$this->add_hook('user_create', array($this, 'user_create'));
// Hooks related to "Login As" feature
$this->add_hook('template_object_loginform', array($this, 'login_form'));
$this->add_hook('imap_connect', array($this, 'imap_connect'));
$this->add_hook('managesieve_connect', array($this, 'imap_connect'));
$this->add_hook('smtp_connect', array($this, 'smtp_connect'));
$this->add_hook('write_log', array($this, 'write_log'));
if ($rcmail->config->get('kolab_auth_auditlog', false)) {
$rcmail->config->set('debug_level', 1);
$rcmail->config->set('devel_mode', true);
$rcmail->config->set('smtp_log', true);
$rcmail->config->set('log_logins', true);
$rcmail->config->set('log_session', true);
$rcmail->config->set('sql_debug', true);
$rcmail->config->set('memcache_debug', true);
$rcmail->config->set('imap_debug', true);
$rcmail->config->set('ldap_debug', true);
$rcmail->config->set('smtp_debug', true);
}
}
public function write_log($args) {
$rcmail = rcmail::get_instance();
if (!$rcmail->config->get('kolab_auth_auditlog', false)) {
return $args;
}
$args['abort'] = true;
if ($rcmail->config->get('log_driver') == 'syslog') {
$prio = $args['name'] == 'errors' ? LOG_ERR : LOG_INFO;
syslog($prio, $args['line']);
return $args;
} else {
$line = sprintf("[%s]: %s\n", $args['date'], $args['line']);
// log_driver == 'file' is assumed here
$log_dir = $rcmail->config->get('log_dir', INSTALL_PATH . 'logs');
// Append original username + target username
if (!is_dir($log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']))) {
// Attempt to create the directory
if (@mkdir($log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']), 0750, true)) {
$log_dir = $log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']);
}
} else {
$log_dir = $log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']);
}
// try to open specific log file for writing
$logfile = $log_dir.'/'.$args['name'];
if ($fp = fopen($logfile, 'a')) {
fwrite($fp, $line);
fflush($fp);
fclose($fp);
return $args;
}
else
trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
}
return $args;
}
/**
* Sets defaults for new user.
*/
public function user_create($args)
{
if (!empty($this->data['user_email']))
$args['user_email'] = $this->data['user_email'];
if (!empty($this->data['user_name']))
$args['user_name'] = $this->data['user_name'];
if (!empty($this->data['user_alias']))
$args['user_alias'] = $this->data['user_alias'];
return $args;
}
/**
* Modifies login form adding additional "Login As" field
*/
public function login_form($args)
{
$this->load_config();
$this->add_texts('localization/');
$rcmail = rcmail::get_instance();
$admin_login = $rcmail->config->get('kolab_auth_admin_login');
$group = $rcmail->config->get('kolab_auth_group');
$role_attr = $rcmail->config->get('kolab_auth_role');
// Show "Login As" input
if (empty($admin_login) || (empty($group) && empty($role_attr))) {
return $args;
}
$input = new html_inputfield(array('name' => '_loginas', 'id' => 'rcmloginas',
'type' => 'text', 'autocomplete' => 'off'));
$row = html::tag('tr', null,
html::tag('td', 'title', html::label('rcmloginas', Q($this->gettext('loginas'))))
. html::tag('td', 'input', $input->show(trim(get_input_value('_loginas', RCUBE_INPUT_POST))))
);
$args['content'] = preg_replace('/<\/tbody>/i', $row . '</tbody>', $args['content']);
return $args;
}
/**
* Find user credentials In LDAP.
*/
public function authenticate($args)
{
$this->load_config();
if (!$this->init_ldap()) {
return $args;
}
$rcmail = rcmail::get_instance();
$admin_login = $rcmail->config->get('kolab_auth_admin_login');
$admin_pass = $rcmail->config->get('kolab_auth_admin_password');
$login_attr = $rcmail->config->get('kolab_auth_login');
$alias_attr = $rcmail->config->get('kolab_auth_alias');
$name_attr = $rcmail->config->get('kolab_auth_name');
// get username and host
$host = rcube_parse_host($args['host']);
$user = $args['user'];
$pass = $args['pass'];
$loginas = trim(get_input_value('_loginas', RCUBE_INPUT_POST));
if (empty($user) || empty($pass)) {
return $args;
}
// Find user record in LDAP
$record = $this->get_user_record($user, $host);
if (empty($record)) {
return $args;
}
// Login As...
if (!empty($loginas) && $admin_login) {
// Authenticate to LDAP
$dn = $this->ldap->dn_decode($record['ID']);
$result = $this->ldap->bind($dn, $pass);
if (!$result) {
return $args;
}
// check if the original user has/belongs to administrative role/group
$isadmin = false;
$group = $rcmail->config->get('kolab_auth_group');
$role_attr = $rcmail->config->get('kolab_auth_role');
$role_dn = $rcmail->config->get('kolab_auth_role_value');
// check role attribute
if (!empty($role_attr) && !empty($role_dn) && !empty($record[$role_attr])) {
$role_dn = $this->parse_vars($role_dn, $user, $host);
foreach ((array)$record[$role_attr] as $role) {
if ($role == $role_dn) {
$isadmin = true;
break;
}
}
}
// check group
if (!$isadmin && !empty($group)) {
$groups = $this->ldap->get_record_groups($record['ID']);
foreach ($groups as $g) {
if ($group == $this->ldap->dn_decode($g)) {
$isadmin = true;
break;
}
}
}
// Save original user login for log (see below)
if ($login_attr)
$origname = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
else
$origname = $user;
$record = null;
// user has the privilage, get "login as" user credentials
if ($isadmin) {
$record = $this->get_user_record($loginas, $host);
}
if (empty($record)) {
$args['valid'] = false;
return $args;
}
$args['user'] = $loginas;
// Mark session to use SASL proxy for IMAP authentication
$_SESSION['kolab_auth_admin'] = strtolower($origname);
$_SESSION['kolab_auth_login'] = $rcmail->encrypt($admin_login);
$_SESSION['kolab_auth_password'] = $rcmail->encrypt($admin_pass);
}
// Set credentials
if ($record) {
if ($login_attr)
$this->data['user_login'] = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
if ($alias_attr)
$this->data['user_alias'] = is_array($record[$alias_attr]) ? $record[$alias_attr][0] : $record[$alias_attr];
if ($name_attr)
$this->data['user_name'] = is_array($record[$name_attr]) ? $record[$name_attr][0] : $record[$name_attr];
if ($this->data['user_login'])
$args['user'] = $this->data['user_login'];
}
// Log "Login As" usage
if (!empty($origname)) {
write_log('userlogins', sprintf('Admin login for %s by %s from %s',
$args['user'], $origname, rcmail_remote_ip()));
}
return $args;
}
/**
* Sets SASL Proxy login/password for IMAP and Managesieve auth
*/
public function imap_connect($args)
{
if (!empty($_SESSION['kolab_auth_admin'])) {
$rcmail = rcmail::get_instance();
$admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
$admin_pass = $rcmail->decrypt($_SESSION['kolab_auth_password']);
$args['auth_cid'] = $admin_login;
$args['auth_pw'] = $admin_pass;
}
return $args;
}
/**
* Sets SASL Proxy login/password for SMTP auth
*/
public function smtp_connect($args)
{
if (!empty($_SESSION['kolab_auth_admin'])) {
$rcmail = rcmail::get_instance();
$admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
$admin_pass = $rcmail->decrypt($_SESSION['kolab_auth_password']);
$args['options']['smtp_auth_cid'] = $admin_login;
$args['options']['smtp_auth_pw'] = $admin_pass;
}
return $args;
}
/**
* Initializes LDAP object and connects to LDAP server
*/
private function init_ldap()
{
if ($this->ldap)
return $this->ldap->ready;
$rcmail = rcmail::get_instance();
$addressbook = $rcmail->config->get('kolab_auth_addressbook');
if (!is_array($addressbook)) {
$ldap_config = (array)$rcmail->config->get('ldap_public');
$addressbook = $ldap_config[$addressbook];
}
if (empty($addressbook)) {
return false;
}
$this->ldap = new kolab_auth_ldap_backend(
$addressbook,
$rcmail->config->get('ldap_debug'),
$rcmail->config->mail_domain($_SESSION['imap_host'])
);
return $this->ldap->ready;
}
/**
* Fetches user data from LDAP addressbook
*/
private function get_user_record($user, $host)
{
$rcmail = rcmail::get_instance();
$filter = $rcmail->config->get('kolab_auth_filter');
$filter = $this->parse_vars($filter, $user, $host);
// reset old result
$this->ldap->reset();
// get record
$this->ldap->set_filter($filter);
$results = $this->ldap->list_records();
if (count($results->records) == 1)
return $results->records[0];
}
/**
* Prepares filter query for LDAP search
*/
private function parse_vars($str, $user, $host)
{
$rcmail = rcmail::get_instance();
$domain = $rcmail->config->get('username_domain');
if (!empty($domain) && strpos($user, '@') === false) {
if (is_array($domain) && isset($domain[$host]))
$user .= '@'.rcube_parse_host($domain[$host], $host);
else if (is_string($domain))
$user .= '@'.rcube_parse_host($domain, $host);
}
// replace variables in filter
list($u, $d) = explode('@', $user);
$dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
$replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u);
return strtr($str, $replaces);
}
}
/**
* Wrapper class for rcube_ldap addressbook
*/
class kolab_auth_ldap_backend extends rcube_ldap
{
function set_filter($filter)
{
if ($filter)
$this->prop['filter'] = $filter;
}
}
diff --git a/plugins/kolab_config/LICENSE b/plugins/kolab_config/LICENSE
new file mode 100644
index 00000000..dba13ed2
--- /dev/null
+++ b/plugins/kolab_config/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, 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
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state 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 program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/plugins/kolab_config/kolab_config.php b/plugins/kolab_config/kolab_config.php
index 03aa3864..7b0947b0 100644
--- a/plugins/kolab_config/kolab_config.php
+++ b/plugins/kolab_config/kolab_config.php
@@ -1,135 +1,144 @@
<?php
/**
* Kolab configuration storage.
*
* Plugin to use Kolab server as a configuration storage. Provides an API to handle
* configuration according to http://wiki.kolab.org/KEP:9.
*
- * Copyright (C) 2011, Kolab Systems AG
+ * @author Machniak Aleksander <machniak@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * @author Machniak Aleksander <machniak@kolabsys.com>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+
class kolab_config extends rcube_plugin
{
public $task = 'utils';
private $config;
private $enabled;
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
$rcmail = rcmail::get_instance();
// Register spellchecker dictionary handlers
if (strtolower($rcmail->config->get('spellcheck_dictionary')) != 'shared') {
$this->add_hook('spell_dictionary_save', array($this, 'dictionary_save'));
$this->add_hook('spell_dictionary_get', array($this, 'dictionary_get'));
}
/*
// Register addressbook saved searches handlers
$this->add_hook('saved_search_create', array($this, 'saved_search_create'));
$this->add_hook('saved_search_delete', array($this, 'saved_search_delete'));
$this->add_hook('saved_search_list', array($this, 'saved_search_list'));
$this->add_hook('saved_search_get', array($this, 'saved_search_get'));
*/
}
/**
* Initializes config object and dependencies
*/
private function load()
{
if ($this->config)
return;
$this->require_plugin('kolab_folders');
// load dependencies
require_once 'Horde/Util.php';
require_once 'Horde/Kolab/Format.php';
require_once 'Horde/Kolab/Format/XML.php';
require_once $this->home . '/lib/configuration.php';
require_once $this->home . '/lib/kolab_configuration.php';
String::setDefaultCharset('UTF-8');
$this->config = new kolab_configuration();
// check if configuration folder exist
if (strlen($this->config->dir)) {
$this->enabled = true;
}
}
/**
* Saves spellcheck dictionary.
*
* @param array $args Hook arguments
*
* @return array Hook arguments
*/
public function dictionary_save($args)
{
$this->load();
if (!$this->enabled) {
return $args;
}
$lang = $args['language'];
$dict = $this->dict;
$dict['type'] = 'dictionary';
$dict['language'] = $args['language'];
$dict['e'] = $args['dictionary'];
if (empty($dict['e'])) {
// Delete the object
$this->config->del($dict);
}
else {
// Update the object
$this->config->set($dict);
}
$args['abort'] = true;
return $args;
}
/**
* Returns spellcheck dictionary.
*
* @param array $args Hook arguments
*
* @return array Hook arguments
*/
public function dictionary_get($args)
{
$this->load();
if (!$this->enabled) {
return $args;
}
$lang = $args['language'];
$this->dict = $this->config->get('dictionary.'.$lang);
if (!empty($this->dict)) {
$args['dictionary'] = $this->dict['e'];
}
$args['abort'] = true;
return $args;
}
}
diff --git a/plugins/kolab_config/lib/configuration.php b/plugins/kolab_config/lib/configuration.php
index e8c127fb..c80fbd3d 100644
--- a/plugins/kolab_config/lib/configuration.php
+++ b/plugins/kolab_config/lib/configuration.php
@@ -1,63 +1,76 @@
<?php
/**
* Kolab XML handler for configuration (KEP:9).
*
- * Copyright (C) 2011 Kolab Systems AG
- *
* @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class Horde_Kolab_Format_XML_configuration extends Horde_Kolab_Format_XML {
/**
* Specific data fields for the configuration object
*
* @var Kolab
*/
var $_fields_specific;
var $_root_version = 2.1;
/**
* Constructor
*/
function Horde_Kolab_Format_XML_configuration($params = array())
{
$this->_root_name = 'configuration';
// Specific configuration fields, in kolab format specification order
$this->_fields_specific = array(
'application' => array (
'type' => HORDE_KOLAB_XML_TYPE_STRING,
'value' => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
),
'type' => array(
'type' => HORDE_KOLAB_XML_TYPE_STRING,
'value' => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
),
);
// Dictionary fields
if (!empty($params['subtype']) && preg_match('/^dictionary.*/', $params['subtype'])) {
$this->_fields_specific = array_merge($this->_fields_specific, array(
'language' => array (
'type' => HORDE_KOLAB_XML_TYPE_STRING,
'value' => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
),
'e' => array(
'type' => HORDE_KOLAB_XML_TYPE_MULTIPLE,
'value' => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
'array' => array(
'type' => HORDE_KOLAB_XML_TYPE_STRING,
'value' => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
),
),
));
}
parent::Horde_Kolab_Format_XML($params);
unset($this->_fields_basic['body']);
unset($this->_fields_basic['categories']);
unset($this->_fields_basic['sensitivity']);
}
}
diff --git a/plugins/kolab_config/lib/kolab_configuration.php b/plugins/kolab_config/lib/kolab_configuration.php
index 9fc4b7b4..c936432c 100644
--- a/plugins/kolab_config/lib/kolab_configuration.php
+++ b/plugins/kolab_config/lib/kolab_configuration.php
@@ -1,232 +1,241 @@
<?php
/**
* Kolab configuration storage handler.
*
- * Copyright (C) 2011, Kolab Systems AG
+ * @author Machniak Aleksander <machniak@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * @author Machniak Aleksander <machniak@kolabsys.com>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_configuration
{
public $dir;
private $rc;
private $imap_data = array();
/**
* Class constructor. Establishes IMAP connection.
*/
public function __construct()
{
$this->rc = rcmail::get_instance();
// Connect to IMAP
$this->rc->imap_connect();
// Get default configuration directory
// @TODO: handle many config directories, according to KEP:9
$dirs = $this->rc->imap->list_unsubscribed('', '*', 'configuration');
$this->dir = array_shift($dirs);
}
/**
* Returns configuration object
*
* @param string $type Configuration object type
*
* @return array Configuration object
*/
public function get($type)
{
// If config folder exists
if (isset($this->dir[0])) {
// search by object type
// @TODO: configuration folders shouldn't be big, consider
// caching of all objects, then if we extend cache with object type
// column we could simply search in cache.
// @TODO: handle many config directories, according to KEP:9
$ctype = 'application/x-vnd.kolab.configuration' . ($type ? '.'.$type : '');
$search = 'HEADER X-Kolab-Type ' . $ctype;
$this->set_imap_props();
$this->rc->imap->search($this->dir, $search, 'US-ASCII', 'date');
$list = $this->rc->imap->list_headers($this->dir, 1, 'date', 'DESC');
$this->reset_imap_props();
foreach ($list as $idx => $obj) {
// @TODO: maybe we could skip parsing the message structure and
// get second part's body directly
$msg = new rcube_message($obj->uid);
$xml = null;
// get XML part
foreach ((array)$msg->attachments as $part) {
if ($part->mimetype == 'application/xml') {
if (!$part->body)
$part->body = $msg->get_part_content($part->mime_id);
$xml = $part->body;
break;
}
}
// that shouldn't happen
if ($xml === null) {
continue;
}
// Load XML object parser
$handler = Horde_Kolab_Format::factory('XML', 'configuration', array('subtype' => $type));
if (is_object($handler) && is_a($handler, 'PEAR_Error')) {
return null;
}
// XML-to-array
$object = $handler->load($xml);
if (is_object($object) && is_a($object, 'PEAR_Error')) {
return null;
}
$object['mailbox'] = $this->dir;
$object['msguid'] = $obj->uid;
return $object;
}
}
}
/**
* Saves configuration object
*
* @param array $object Configuration object
*
* @return bool True on success, False on failre
*/
public function set($object)
{
$object['uid'] = md5(uniqid(mt_rand(), true));
$raw_msg = $this->build_message($object);
if ($raw_msg && isset($this->dir[0])) {
$result = $this->rc->imap->save_message($this->dir, $raw_msg, '', false);
}
// delete old message
if ($result && !empty($object['msguid'])) {
$this->rc->imap->delete_message($object['msguid'], $object['mailbox']);
}
return $result;
}
/**
* Deletes configuration object
*
* @param array $object Configuration object
*
* @return bool True on success, False on failre
*/
public function del($object)
{
if (!empty($object['msguid'])) {
return $this->rc->imap->delete_message($object['msguid'], $object['mailbox']);
}
return true;
}
/**
* Sets some rcube_imap settings before searching for config objects
*/
private function set_imap_props()
{
// Save old settings
$this->imap_data = array(
'skip_deleted' => $skip_deleted,
'page_size' => $page_size,
'list_page' => $list_page,
'threading' => $threading,
);
// Configure for configuration folder
$this->rc->imap->skip_deleted = true;
$this->rc->imap->threading = false;
$this->rc->imap->page_size = 100;
$this->rc->imap->list_page = 1;
}
/**
* Resets rcube_imap settings after searching for config objects
*/
private function reset_imap_props()
{
// Reset to old settings
foreach ($this->imap_data as $key => $val) {
$this->rc->imap->$key = $val;
}
// reset saved search
$this->rc->imap->search_set = null;
}
/**
* Creates source of the configuration object message
*/
private function build_message($object)
{
$MAIL_MIME = new Mail_mime("\r\n");
$name = 'configuration';
if (!empty($object['type'])) {
$name .= '.' . $object['type'];
if ($object['type'] == 'dictionary' && !empty($object['language'])) {
$name .= '.' . $object['language'];
}
}
$handler = Horde_Kolab_Format::factory('XML', 'configuration',
array('subtype' => $object['type']));
if (is_object($handler) && is_a($handler, 'PEAR_Error')) {
return false;
}
$xml = $handler->save($object);
if (is_object($xml) && is_a($xml, 'PEAR_Error')) {
return false;
}
if ($ident = $this->rc->user->get_identity()) {
$headers['From'] = $ident['email'];
$headers['To'] = $ident['email'];
}
$headers['Date'] = date('r');
$headers['X-Kolab-Type'] = 'application/x-vnd.kolab.'.$name;
$headers['Subject'] = $object['uid'];
$headers['Message-ID'] = rcmail_gen_message_id();
$headers['User-Agent'] = $this->rc->config->get('useragent');
$MAIL_MIME->headers($headers);
$MAIL_MIME->setTXTBody('This is a Kolab Groupware object. '
.'To view this object you will need an email client that understands the Kolab Groupware format. '
.'For a list of such email clients please visit http://www.kolab.org/kolab2-clients.html');
$MAIL_MIME->addAttachment($xml,
'application/xml',
$name . '.xml',
false, '8bit', 'attachment', RCMAIL_CHARSET, '', '',
$this->rc->config->get('mime_param_folding') ? 'quoted-printable' : null,
$this->rc->config->get('mime_param_folding') == 2 ? 'quoted-printable' : null,
'', RCMAIL_CHARSET
);
return $MAIL_MIME->getMessage();
}
}
diff --git a/plugins/kolab_core/LICENSE b/plugins/kolab_core/LICENSE
new file mode 100644
index 00000000..dba13ed2
--- /dev/null
+++ b/plugins/kolab_core/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, 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
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state 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 program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/plugins/kolab_core/kolab_core.php b/plugins/kolab_core/kolab_core.php
index 8ff82e87..b027a485 100644
--- a/plugins/kolab_core/kolab_core.php
+++ b/plugins/kolab_core/kolab_core.php
@@ -1,46 +1,54 @@
<?php
/**
* Kolab core library
*
* Plugin to setup a basic environment for interaction with a Kolab server.
* Other Kolab-related plugins will depend on it and can use the static API rcube_core
*
- * Copyright (C) 2011, Kolab Systems AG
+ * @author Thomas Bruederli <roundcube@gmail.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * @author Thomas Bruederli <roundcube@gmail.com>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
*
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+
class kolab_core extends rcube_plugin
{
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
// load local config
$this->load_config();
// extend include path to load bundled Horde classes
$include_path = $this->home . PATH_SEPARATOR . ini_get('include_path');
set_include_path($include_path);
// Register password reset hook
$this->add_hook('password_change', array($this, 'password_change'));
}
/**
* Resets auth session data after password change
*/
public function password_change($args)
{
rcmail::get_instance()->session->remove('__auth');
return $args;
}
}
-
diff --git a/plugins/kolab_core/rcube_kolab.php b/plugins/kolab_core/rcube_kolab.php
index 72655b48..c64b02db 100644
--- a/plugins/kolab_core/rcube_kolab.php
+++ b/plugins/kolab_core/rcube_kolab.php
@@ -1,561 +1,572 @@
<?php
/**
- * Copyright (C) 2011, Kolab Systems AG
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * Utilities providing Kolab functionality using Kolab_* classes
+ * from the Horde project.
*
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
ini_set('error_reporting', E_ALL&~(E_DEPRECATED | E_NOTICE));
require_once 'Horde/Kolab/Storage/List.php';
require_once 'Horde/Kolab/Format.php';
require_once 'Horde/Auth.php';
require_once 'Horde/Auth/kolab.php';
require_once 'Horde/Perms.php';
/**
* Glue class to handle access to the Kolab data using the Kolab_* classes
* from the Horde project.
*/
class rcube_kolab
{
public static $last_error;
private static $horde_auth;
private static $config;
private static $ready = false;
private static $list;
private static $cache;
/**
* Setup the environment needed by the Kolab_* classes to access Kolab data
*/
public static function setup()
{
global $conf;
// setup already done
if (self::$horde_auth)
return;
$rcmail = rcmail::get_instance();
// get password of logged user
$pwd = $rcmail->decrypt($_SESSION['password']);
// load ldap credentials from local config
$conf['kolab'] = (array) $rcmail->config->get('kolab');
// Set global Horde config (e.g. Cache settings)
if (!empty($conf['kolab']['global'])) {
$conf = array_merge($conf, $conf['kolab']['global']);
unset($conf['kolab']['global']);
}
// Set Horde configuration (for cache db)
$dsnw = $rcmail->config->get('db_dsnw');
$dsnr = $rcmail->config->get('db_dsnr');
$conf['sql'] = MDB2::parseDSN($dsnw);
$conf['sql']['charset'] = 'utf-8';
if (!empty($dsnr) && $dsnr != $dsnw) {
$conf['sql']['read'] = MDB2::parseDSN($dsnr);
$conf['sql']['read']['charset'] = 'utf-8';
$conf['sql']['splitread'] = true;
}
// Re-set LDAP/IMAP host config
$ldap = array('server' => 'ldap://' . $_SESSION['imap_host'] . ':389');
$imap = array('server' => $_SESSION['imap_host'], 'port' => $_SESSION['imap_port']);
$freebusy = array('server' => 'https://' . $_SESSION['imap_host'] . '/freebusy');
$conf['kolab']['ldap'] = array_merge($ldap, (array)$conf['kolab']['ldap']);
$conf['kolab']['imap'] = array_merge($imap, (array)$conf['kolab']['imap']);
$conf['kolab']['freebusy'] = array_merge($freebusy, (array)$conf['kolab']['freebusy']);
self::$config = &$conf;
// pass the current IMAP authentication credentials to the Horde auth system
self::$horde_auth = Auth::singleton('kolab');
$username = $_SESSION['username'];
$credentials = array('password' => $pwd);
// Hack proxy auth for "Login As" feature of kolab_auth plugin
if (!empty($_SESSION['kolab_auth_admin'])) {
$username = $_SESSION['kolab_auth_admin'];
$conf['kolab']['imap']['user'] = $_SESSION['username'];
$conf['kolab']['imap']['authuser'] = $rcmail->decrypt($_SESSION['kolab_auth_login']);
$conf['kolab']['imap']['password'] = $rcmail->decrypt($_SESSION['kolab_auth_password']);
$conf['kolab']['user_mail'] = $_SESSION['username'];
}
if (self::$horde_auth->isAuthenticated()) {
self::$ready = true;
}
else if (self::$horde_auth->authenticate($username, $credentials, false)) {
// we could use Auth::setAuth() here, but it requires the whole bunch
// of includes and global objects, do it as simple as possible
$_SESSION['__auth'] = array(
'authenticated' => true,
'userId' => $_SESSION['username'],
'timestamp' => time(),
'remote_addr' => $_SERVER['REMOTE_ADDR'],
);
Auth::setCredential('password', $pwd);
self::$ready = true;
}
else {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => sprintf("Unable to authenticate user %s!", $_SESSION['username'])),
true, true);
}
// Register shutdown function for saving cache/session objects
$rcmail->add_shutdown_function(array('rcube_kolab', 'shutdown'));
NLS::setCharset('UTF-8');
String::setDefaultCharset('UTF-8');
}
/**
* Get instance of Kolab_List object
*
* @return object Kolab_List Folders list object
*/
public static function get_folders_list()
{
self::setup();
if (self::$list)
return self::$list;
if (!self::$ready)
return null;
$rcmail = rcmail::get_instance();
$imap_cache = $rcmail->config->get('imap_cache');
if ($imap_cache) {
self::$cache = $rcmail->get_cache('IMAP', $imap_cache);
self::$list = self::$cache->get('mailboxes.kolab');
// Disable Horde folders caching, we're using our own cache
self::$config['kolab']['imap']['cache_folders'] = false;
}
if (empty(self::$list)) {
self::$list = Kolab_List::singleton();
}
return self::$list;
}
/**
* Get instance of a Kolab (XML) format object
*
* @param string Data type (contact,event,task,note)
*
* @return object Horde_Kolab_Format_XML The format object
*/
public static function get_format($type)
{
self::setup();
return Horde_Kolab_Format::factory('XML', $type);
}
/**
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,event,task,note)
*
* @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
*/
public static function get_folders($type)
{
$kolab = self::get_folders_list();
return self::$ready ? $kolab->getByType($type) : array();
}
/**
* Getter for a specific storage folder
*
* @param string IMAP folder to access (UTF7-IMAP)
* @return object Kolab_Folder The folder object
*/
public static function get_folder($folder)
{
$kolab = self::get_folders_list();
return self::$ready ? $kolab->getFolder($folder) : null;
}
/**
* Checks if the given folder is subscribed.
* Much nicer would be if the Kolab_Folder object could tell this information
*
* @param string Full IMAP folder name
* @return boolean True if in the list of subscribed folders, False if not
*/
public static function is_subscribed($folder)
{
static $subscribed; // local cache
if (!$subscribed) {
$rcmail = rcmail::get_instance();
// try without connection first (list could be served from cache)
$subscribed = $rcmail->imap ? $rcmail->imap->list_mailboxes() : array();
// now really get the list from the IMAP server
if (empty($subscribed) || $subscribed == array('INBOX')) {
$rcmail->imap_connect();
$subscribed = $rcmail->imap->list_mailboxes();
}
}
return in_array($folder, $subscribed);
}
/**
* Get storage object for read/write access to the Kolab backend
*
* @param string IMAP folder to access (UTF7-IMAP)
* @param string Object type to deal with (leave empty for auto-detection using annotations)
*
* @return object Kolab_Data The data storage object
*/
public static function get_storage($folder, $data_type = null)
{
$kolab = self::get_folders_list();
return self::$ready ? $kolab->getFolder($folder)->getData($data_type) : null;
}
/**
* Compose an URL to query the free/busy status for the given user
*/
public static function get_freebusy_url($email)
{
return unslashify(self::$config['kolab']['freebusy']['server']) . '/' . $email . '.ifb';
}
/**
* Do session/cache operations on shutdown
*/
public static function shutdown()
{
if (self::$ready) {
// Horde's shutdown function doesn't work with Roundcube session
// save Horde_Kolab_Session object in session here
require_once 'Horde/SessionObjects.php';
$session = Horde_SessionObjects::singleton();
$kolab = Horde_Kolab_Session::singleton();
$session->overwrite('kolab_session', $kolab, false);
// Write Kolab_List object to cache
if (self::$cache && self::$list) {
self::$cache->set('mailboxes.kolab', self::$list);
}
}
}
/**
* Creates folder ID from folder name
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder ID string
*/
public static function folder_id($folder)
{
return asciiwords(strtr($folder, '/.', '--'));
}
/**
* Deletes IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_delete($name)
{
$kolab = self::get_folders_list();
$folder = $kolab->getFolder($name);
$result = $folder->delete();
if (is_object($result) && is_a($result, 'PEAR_Error')) {
self::$last_error = $result->getMessage();
return false;
}
return true;
}
/**
* Creates IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Folder type
* @param bool $default True if older is default (for specified type)
*
* @return bool True on success, false on failure
*/
public static function folder_create($name, $type=null, $default=false)
{
$kolab = self::get_folders_list();
$folder = new Kolab_Folder();
$folder->setList($kolab);
$folder->setFolder($name);
$result = $folder->save(array(
'type' => $type,
'default' => $default,
));
if (is_object($result) && is_a($result, 'PEAR_Error')) {
self::$last_error = $result->getMessage();
return false;
}
return true;
}
/**
* Renames IMAP folder
*
* @param string $oldname Old folder name (UTF7-IMAP)
* @param string $newname New folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_rename($oldname, $newname)
{
$kolab = self::get_folders_list();
$folder = $kolab->getFolder($oldname);
$folder->setFolder($newname);
$result = $kolab->rename($folder);
if (is_object($result) && is_a($result, 'PEAR_Error')) {
self::$last_error = $result->getMessage();
return false;
}
// @TODO: Horde doesn't update subfolders cache nor subscriptions
// but we cannot use Roundcube imap object here, because
// when two connections are used in one request and we have
// multi-server configuration, updating the cache after all
// would get wrong information (e.g. annotations)
// Reset the List object and cache
$kolab = null;
if (self::$cache) {
self::$list = null;
self::$cache->remove('mailboxes', true);
}
return true;
}
/**
* Getter for human-readable name of Kolab object (folder)
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_name($folder, &$folder_ns=null)
{
$rcmail = rcmail::get_instance();
$rcmail->imap_init();
$namespace = $rcmail->imap->get_namespace();
$found = false;
if (!empty($namespace['shared'])) {
foreach ($namespace['shared'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
$prefix = '';
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
$found = true;
$folder_ns = 'shared';
break;
}
}
}
if (!$found && !empty($namespace['other'])) {
foreach ($namespace['other'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
// get username
$pos = strpos($folder, $delim);
if ($pos) {
$prefix = '('.substr($folder, 0, $pos).') ';
$folder = substr($folder, $pos+1);
}
else {
$prefix = '('.$folder.')';
$folder = '';
}
$found = true;
$folder_ns = 'other';
break;
}
}
}
if (!$found && !empty($namespace['personal'])) {
foreach ($namespace['personal'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$prefix = '';
$delim = $ns[1];
$found = true;
break;
}
}
}
if (empty($delim))
$delim = $rcmail->imap->get_hierarchy_delimiter();
$folder = rcube_charset_convert($folder, 'UTF7-IMAP');
$folder = str_replace($delim, ' &raquo; ', $folder);
if ($prefix)
$folder = $prefix . ' ' . $folder;
if (!$folder_ns)
$folder_ns = 'personal';
return $folder;
}
/**
* Getter for the name of the namespace to which the IMAP folder belongs
*
* @param string $folder IMAP folder name (UTF7-IMAP)
*
* @return string Name of the namespace (personal, other, shared)
*/
public static function folder_namespace($folder)
{
$rcmail = rcmail::get_instance();
$rcmail->imap_init();
$namespace = $rcmail->imap->get_namespace();
if (!empty($namespace)) {
foreach ($namespace as $nsname => $nsvalue) {
if (in_array($nsname, array('personal', 'other', 'shared')) && !empty($nsvalue)) {
foreach ($nsvalue as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
return $namespace = $nsname;
}
}
}
}
}
return 'personal';
}
/**
* Creates a SELECT field with folders list
*
* @param string $type Folder type
* @param array $attrs SELECT field attributes (e.g. name)
* @param string $current The name of current folder (to skip it)
*
* @return html_select SELECT object
*/
public static function folder_selector($type, $attrs, $current = '')
{
// get all folders of specified type
$folders = self::get_folders($type);
$delim = $_SESSION['imap_delimiter'];
$names = array();
$len = strlen($current);
if ($len && ($rpos = strrpos($current, $delim))) {
$parent = substr($current, 0, $rpos);
$p_len = strlen($parent);
}
// Filter folders list
foreach ($folders as $c_folder) {
$name = $c_folder->name;
// skip current folder and it's subfolders
if ($len && ($name == $current || strpos($name, $current.$delim) === 0)) {
continue;
}
// always show the parent of current folder
if ($p_len && $name == $parent) { }
// skip folders where user have no rights to create subfolders
else if ($c_folder->getOwner() != $_SESSION['username']) {
$rights = $c_folder->getMyRights();
if (PEAR::IsError($rights) || !preg_match('/[ck]/', $rights)) {
continue;
}
}
$names[$name] = rcube_charset_convert($name, 'UTF7-IMAP');
}
// Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
if ($p_len && !isset($names[$parent])) {
$names[$parent] = rcube_charset_convert($parent, 'UTF7-IMAP');
}
// Sort folders list
asort($names, SORT_LOCALE_STRING);
$folders = array_keys($names);
$names = array();
// Build SELECT field of parent folder
$select = new html_select($attrs);
$select->add('---', '');
foreach ($folders as $name) {
$imap_name = $name;
$name = $origname = self::object_name($name);
// find folder prefix to truncate
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i].' &raquo; ') === 0) {
$length = strlen($names[$i].' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
break;
}
}
$names[] = $origname;
$select->add($name, $imap_name);
}
return $select;
}
}
diff --git a/plugins/kolab_folders/LICENSE b/plugins/kolab_folders/LICENSE
new file mode 100644
index 00000000..dba13ed2
--- /dev/null
+++ b/plugins/kolab_folders/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, 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
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state 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 program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/plugins/kolab_folders/kolab_folders.php b/plugins/kolab_folders/kolab_folders.php
index 01c025cf..16f89b0b 100644
--- a/plugins/kolab_folders/kolab_folders.php
+++ b/plugins/kolab_folders/kolab_folders.php
@@ -1,552 +1,551 @@
<?php
/**
* Type-aware folder management/listing for Kolab
*
* @version 0.3
* @author Aleksander Machniak <machniak@kolabsys.com>
*
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * Copyright (C) 2011, Kolab Systems AG
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_folders extends rcube_plugin
{
public $task = '?(?!login).*';
public $types = array('mail', 'event', 'journal', 'task', 'note', 'contact', 'configuration');
public $mail_types = array('drafts', 'sentitems', 'outbox', 'wastebasket', 'junkemail');
private $rc;
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
/**
* Plugin initialization.
*/
function init()
{
$this->rc = rcmail::get_instance();
// Folder listing hooks
$this->add_hook('mailboxes_list', array($this, 'mailboxes_list'));
// Folder manager hooks
$this->add_hook('folder_form', array($this, 'folder_form'));
$this->add_hook('folder_update', array($this, 'folder_save'));
$this->add_hook('folder_create', array($this, 'folder_save'));
$this->add_hook('folder_delete', array($this, 'folder_save'));
$this->add_hook('folder_rename', array($this, 'folder_save'));
$this->add_hook('folders_list', array($this, 'folders_list'));
}
/**
* Handler for mailboxes_list hook. Enables type-aware lists filtering.
*/
function mailboxes_list($args)
{
if (!$this->metadata_support()) {
return $args;
}
$filter = $args['filter'];
// all-folders request, use core method
if (!$filter) {
return $args;
}
// get folders types
$folderdata = $this->get_folder_type_list($args['root'].$args['name']);
if (!is_array($folderdata)) {
$args['folders'] = false;
return $args;
}
$regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
// In some conditions we can skip LIST command (?)
if ($args['mode'] == 'LIST' && $filter != 'mail'
&& $args['root'] == '' && $args['name'] == '*'
) {
foreach ($folderdata as $folder => $data) {
if (!preg_match($regexp, $data[kolab_folders::CTYPE_KEY])) {
unset ($folderdata[$folder]);
}
}
$args['folders'] = array_keys($folderdata);
return $args;
}
// Get folders list
if ($args['mode'] == 'LIST') {
$args['folders'] = $this->rc->imap->conn->listMailboxes($args['root'], $args['name']);
}
else {
$args['folders'] = $this->list_subscribed($args['root'], $args['name']);
}
// In case of an error, return empty list
if (!is_array($args['folders'])) {
$args['folders'] = array();
return $args;
}
// Filter folders list
foreach ($args['folders'] as $idx => $folder) {
$data = $folderdata[$folder];
// Empty data => mail
if ($filter == 'mail' && empty($data)) {
continue;
}
if (empty($data) || !preg_match($regexp, $data[kolab_folders::CTYPE_KEY])) {
unset($args['folders'][$idx]);
}
}
return $args;
}
/**
* Handler for folders_list hook. Add css classes to folder rows.
*/
function folders_list($args)
{
if (!$this->metadata_support()) {
return $args;
}
$table = $args['table'];
// get folders types
$folderdata = $this->get_folder_type_list('*');
if (!is_array($folderdata)) {
return $args;
}
// Add type-based style for table rows
// See kolab_folders::folder_class_name()
for ($i=1, $cnt=$table->size(); $i<=$cnt; $i++) {
$attrib = $table->get_row_attribs($i);
$folder = $attrib['foldername']; // UTF7-IMAP
$data = $folderdata[$folder];
if (!empty($data))
$type = $data[kolab_folders::CTYPE_KEY];
if (!$type)
$type = 'mail';
$class_name = self::folder_class_name($type);
$attrib['class'] = trim($attrib['class'] . ' ' . $class_name);
$table->set_row_attribs($attrib, $i);
}
return $args;
}
/**
* Handler for folder info/edit form (folder_form hook).
* Adds folder type selector.
*/
function folder_form($args)
{
if (!$this->metadata_support()) {
return $args;
}
// load translations
$this->add_texts('localization/', false);
// INBOX folder is of type mail.inbox and this cannot be changed
if ($args['name'] == 'INBOX') {
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => sprintf('%s (%s)', $this->gettext('foldertypemail'), $this->gettext('inbox')),
);
return $args;
}
if ($args['options']['is_root']) {
return $args;
}
$mbox = strlen($args['name']) ? $args['name'] : $args['parent_name'];
if (isset($_POST['_ctype'])) {
$new_ctype = trim(get_input_value('_ctype', RCUBE_INPUT_POST));
$new_subtype = trim(get_input_value('_subtype', RCUBE_INPUT_POST));
}
// Get type of the folder or the parent
if (strlen($mbox)) {
list($ctype, $subtype) = $this->get_folder_type($mbox);
if (strlen($args['parent_name']) && $subtype == 'default')
$subtype = ''; // there can be only one
}
if (!$ctype) {
$ctype = 'mail';
}
// Don't allow changing type of shared folder, according to ACL
if (strlen($mbox)) {
$options = $this->rc->imap->mailbox_info($mbox);
if ($options['namespace'] != 'personal' && !in_array('a', $options['rights'])) {
if (in_array($ctype, $this->types))
$value = $this->gettext('foldertype'.$ctype);
else
$value = $ctype;
if ($subtype)
$value .= ' ('. ($subtype == 'default' ? $this->gettext('default') : $subtype) .')';
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => $value,
);
return $args;
}
}
// Add javascript script to the client
$this->include_script('kolab_folders.js');
// build type SELECT fields
$type_select = new html_select(array('name' => '_ctype', 'id' => '_ctype'));
$sub_select = new html_select(array('name' => '_subtype', 'id' => '_subtype'));
foreach ($this->types as $type) {
$type_select->add($this->gettext('foldertype'.$type), $type);
}
// add non-supported type
if (!in_array($ctype, $this->types)) {
$type_select->add($ctype, $ctype);
}
$sub_select->add('', '');
$sub_select->add($this->gettext('default'), 'default');
foreach ($this->mail_types as $type) {
$sub_select->add($this->gettext($type), $type);
}
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => $type_select->show(isset($new_ctype) ? $new_ctype : $ctype)
. $sub_select->show(isset($new_subtype) ? $new_subtype : $subtype),
);
return $args;
}
/**
* Handler for folder update/create action (folder_update/folder_create hook).
*/
function folder_save($args)
{
// Folder actions from folders list
if (empty($args['record'])) {
// Just clear Horde folders cache and return
$this->clear_folders_cache();
return $args;
}
// Folder create/update with form
$ctype = trim(get_input_value('_ctype', RCUBE_INPUT_POST));
$subtype = trim(get_input_value('_subtype', RCUBE_INPUT_POST));
$mbox = $args['record']['name'];
$old_mbox = $args['record']['oldname'];
$subscribe = $args['record']['subscribe'];
if (empty($ctype)) {
return $args;
}
// load translations
$this->add_texts('localization/', false);
// Skip folder creation/rename in core
// @TODO: Maybe we should provide folder_create_after and folder_update_after hooks?
// Using create_mailbox/rename_mailbox here looks bad
$args['abort'] = true;
// There can be only one default folder of specified type
if ($subtype == 'default') {
$default = $this->get_default_folder($ctype);
if ($default !== null && $old_mbox != $default) {
$args['result'] = false;
$args['message'] = $this->gettext('defaultfolderexists');
return $args;
}
}
// Subtype sanity-checks
else if ($subtype && ($ctype != 'mail' || !in_array($subtype, $this->mail_types))) {
$subtype = '';
}
$ctype .= $subtype ? '.'.$subtype : '';
// Create folder
if (!strlen($old_mbox)) {
// By default don't subscribe to non-mail folders
if ($subscribe)
$subscribe = (bool) preg_match('/^mail/', $ctype);
$result = $this->rc->imap->create_mailbox($mbox, $subscribe);
// Set folder type
if ($result) {
$this->set_folder_type($mbox, $ctype);
}
}
// Rename folder
else {
if ($old_mbox != $mbox) {
$result = $this->rc->imap->rename_mailbox($old_mbox, $mbox);
}
else {
$result = true;
}
if ($result) {
list($oldtype, $oldsubtype) = $this->get_folder_type($mbox);
$oldtype .= $oldsubtype ? '.'.$oldsubtype : '';
if ($ctype != $oldtype) {
$this->set_folder_type($mbox, $ctype);
}
}
}
// Clear Horde folders cache
if ($result) {
$this->clear_folders_cache();
}
$args['record']['class'] = self::folder_class_name($ctype);
$args['record']['subscribe'] = $subscribe;
$args['result'] = $result;
return $args;
}
/**
* Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
*
* @return boolean
*/
function metadata_support()
{
return $this->rc->imap->get_capability('METADATA') ||
$this->rc->imap->get_capability('ANNOTATEMORE') ||
$this->rc->imap->get_capability('ANNOTATEMORE2');
}
/**
* Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
*
* @param string $folder Folder name
*
* @return array Folder content-type
*/
function get_folder_type($folder)
{
$folderdata = $this->rc->imap->get_metadata($folder, array(kolab_folders::CTYPE_KEY));
return explode('.', $folderdata[$folder][kolab_folders::CTYPE_KEY]);
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return boolean True on success
*/
function set_folder_type($folder, $type='mail')
{
return $this->rc->imap->set_metadata($folder, array(kolab_folders::CTYPE_KEY => $type));
}
/**
* Returns list of subscribed folders (directly from IMAP server)
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
*
* @return array List of mailboxes/folders
*/
private function list_subscribed($root='', $name='*')
{
$imap = $this->rc->imap;
// Code copied from rcube_imap::_list_mailboxes()
// Server supports LIST-EXTENDED, we can use selection options
// #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
if (!$this->rc->config->get('imap_force_lsub') && $imap->get_capability('LIST-EXTENDED')) {
// This will also set mailbox options, LSUB doesn't do that
$a_folders = $imap->conn->listMailboxes($root, $name,
NULL, array('SUBSCRIBED'));
// remove non-existent folders
if (is_array($a_folders) && $name = '*') {
foreach ($a_folders as $idx => $folder) {
if ($imap->conn->data['LIST'] && ($opts = $imap->conn->data['LIST'][$folder])
&& in_array('\\NonExistent', $opts)
) {
$imap->conn->unsubscribe($folder);
unset($a_folders[$idx]);
}
}
}
}
// retrieve list of folders from IMAP server using LSUB
else {
$a_folders = $imap->conn->listSubscribed($root, $name);
// unsubscribe non-existent folders, remove from the list
if (is_array($a_folders) && $name == '*') {
foreach ($a_folders as $idx => $folder) {
if ($imap->conn->data['LIST'] && ($opts = $imap->conn->data['LIST'][$folder])
&& in_array('\\Noselect', $opts)
) {
// Some servers returns \Noselect for existing folders
if (!$imap->mailbox_exists($folder)) {
$imap->conn->unsubscribe($folder);
unset($a_folders[$idx]);
}
}
}
}
}
return $a_folders;
}
/**
* Returns list of folder(s) type(s)
*
* @param string $mbox Folder name or pattern
*
* @return array List of folders data, indexed by folder name
*/
function get_folder_type_list($mbox)
{
// Use mailboxes. prefix so the cache will be cleared by core
// together with other mailboxes-related cache data
$cache_key = 'mailboxes.types.'.$mbox;
// get cached metadata
$metadata = $this->rc->imap->get_cache($cache_key);
if (is_array($metadata)) {
return $metadata;
}
$metadata = $this->rc->imap->get_metadata($mbox, kolab_folders::CTYPE_KEY);
if (!is_array($metadata)) {
return false;
}
// write mailboxlist to cache
$this->rc->imap->update_cache($cache_key, $metadata);
return $metadata;
}
/**
* Returns the name of default folder
*
* @param string $type Folder type
*
* @return string Folder name
*/
function get_default_folder($type)
{
$folderdata = $this->get_folder_type_list('*');
if (!is_array($folderdata)) {
return null;
}
$type .= '.default';
$namespace = $this->rc->imap->get_namespace();
$delimiter = $this->rc->imap->get_hierarchy_delimiter();
foreach ($folderdata as $folder => $data) {
if ($data[kolab_folders::CTYPE_KEY] != $type) {
unset ($folderdata[$folder]);
continue;
}
// folder found, check if it is in personal namespace
$fname = $folder . $delimiter;
if (!empty($namespace['other'])) {
foreach ($namespace['other'] as $item) {
if ($item[0] === $fname) {
unset ($folderdata[$folder]);
continue 2;
}
}
}
if (!empty($namespace['shared'])) {
foreach ($namespace['shared'] as $item) {
if ($item[0] === $fname) {
unset ($folderdata[$folder]);
continue 2;
}
}
}
// There can be only one default folder of specified type
return $folder;
}
return null;
}
/**
* Returns CSS class name for specified folder type
*
* @param string $type Folder type
*
* @return string Class name
*/
static function folder_class_name($type)
{
list($ctype, $subtype) = explode('.', $type);
$class[] = 'type-' . ($ctype ? $ctype : 'mail');
if ($subtype)
$class[] = 'subtype-' . $subtype;
return implode(' ', $class);
}
/**
* Clear Horde's folder cache. See Kolab_List::singleton().
*/
private function clear_folders_cache()
{
unset($_SESSION['horde_session_objects']['kolab_folderlist']);
}
}
diff --git a/plugins/kolab_zpush/LICENSE b/plugins/kolab_zpush/LICENSE
new file mode 100644
index 00000000..dba13ed2
--- /dev/null
+++ b/plugins/kolab_zpush/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, 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
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state 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 program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/plugins/kolab_zpush/kolab_zpush.js b/plugins/kolab_zpush/kolab_zpush.js
index ced5a0d3..76c89f4d 100644
--- a/plugins/kolab_zpush/kolab_zpush.js
+++ b/plugins/kolab_zpush/kolab_zpush.js
@@ -1,164 +1,172 @@
/**
* Client scripts for the Kolab Z-Push configuration utitlity
*
* @version 0.2
* @author Thomas Bruederli <roundcube@gmail.com>
*
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
function kolab_zpush_config()
{
/* private members */
var me = this;
var http_lock = null;
var active_device = null;
/* constructor */
var devicelist = new rcube_list_widget(rcmail.gui_objects.devicelist,
{ multiselect:true, draggable:false, keyboard:true });
devicelist.addEventListener('select', select_device);
devicelist.init();
rcmail.register_command('plugin.save-config', save_config);
rcmail.register_command('plugin.delete-device', delete_device_config);
rcmail.addEventListener('plugin.zpush_data_ready', device_data_ready);
rcmail.addEventListener('plugin.zpush_save_complete', save_complete);
$('input.subscription').change(function(e){ $('#'+this.id+'_alarm').prop('disabled', !this.checked); });
$(window).bind('resize', resize_ui);
// select the one and only device from list
if (rcmail.env.devicecount == 1) {
for (var imei in rcmail.env.devices)
break;
devicelist.select(imei);
}
/* private methods */
function select_device(list)
{
active_device = list.get_single_selection();
rcmail.enable_command('plugin.save-config', 'plugin.delete-device', true);
if (active_device) {
http_lock = rcmail.set_busy(true, 'loading');
rcmail.http_request('plugin.zpushjson', { cmd:'load', id:active_device }, http_lock);
}
$('#introtext').hide();
}
// callback from server after loading device data
function device_data_ready(data)
{
// reset form first
$('input.alarm:checked').prop('checked', false);
$('input.subscription:checked').prop('checked', false).change();
if (data.id && data.id == active_device) {
$('#config-device-alias').val(data.devicealias);
$('#config-device-mode').val(data.syncmode);
$('#config-device-laxpic').prop('checked', data.laxpic ? true : false);
$('input.subscription').each(function(i, elem){
var key = elem.value;
elem.checked = data.subscribed[key] ? true : false;
}).change();
$('input.alarm').each(function(i, elem){
var key = elem.value;
elem.checked = data.subscribed[key] == 2;
});
$('#configform, #prefs-box .boxtitle').show();
resize_ui();
}
else {
$('#configform, #prefs-box .boxtitle').hide();
}
}
// submit current configuration form to server
function save_config()
{
// TODO: validate device info
var data = {
cmd: 'save',
id: active_device,
devicealias: $('#config-device-alias').val(),
syncmode: $('#config-device-mode option:selected').val(),
laxpic: $('#config-device-laxpic').get(0).checked ? 1 : 0
};
data.subscribed = {};
$('input.subscription:checked').each(function(i, elem){
data.subscribed[elem.value] = 1;
});
$('input.alarm:checked').each(function(i, elem){
if (data.subscribed[elem.value])
data.subscribed[elem.value] = 2;
});
http_lock = rcmail.set_busy(true, 'kolab_zpush.savingdata');
rcmail.http_post('plugin.zpushjson', data, http_lock);
}
// callback function when saving has completed
function save_complete(p)
{
if (p.success && p.devicealias) {
$('#devices-table tr.selected span.devicealias').html(p.devicealias);
rcmail.env.devices[p.id].ALIAS = p.devicealias;
}
}
// handler for delete commands
function delete_device_config()
{
if (active_device && confirm(rcmail.gettext('devicedeleteconfirm', 'kolab_zpush'))) {
http_lock = rcmail.set_busy(true, 'kolab_zpush.savingdata');
rcmail.http_post('plugin.zpushjson', { cmd:'delete', id:active_device }, http_lock);
}
}
// handler for window resize events: sets max-height of folders list scroll container
function resize_ui()
{
if (active_device) {
var h = $(window).height();
var pos = $('#foldersubscriptions').offset();
$('#foldersubscriptions').css('max-height', (h - pos.top - 90) + 'px');
}
}
}
window.rcmail && rcmail.addEventListener('init', function(evt) {
var ACTION_CONFIG = 'plugin.zpushconfig';
// add button to tabs list
var tab = $('<span>').attr('id', 'settingstabpluginzpushconfig').addClass('tablink');
var button = $('<a>').attr('href', rcmail.env.comm_path+'&_action=plugin.zpushconfig').html(rcmail.gettext('tabtitle', 'kolab_zpush')).appendTo(tab);
rcmail.add_element(tab, 'tabs');
if (rcmail.env.action == ACTION_CONFIG)
new kolab_zpush_config();
});
// extend jQuery
(function($){
$.fn.serializeJSON = function(){
var json = {};
jQuery.map($(this).serializeArray(), function(n, i) {
json[n['name']] = n['value'];
});
return json;
};
})(jQuery);
-
diff --git a/plugins/kolab_zpush/kolab_zpush.php b/plugins/kolab_zpush/kolab_zpush.php
index 9ecd1b8c..88b41040 100644
--- a/plugins/kolab_zpush/kolab_zpush.php
+++ b/plugins/kolab_zpush/kolab_zpush.php
@@ -1,345 +1,344 @@
<?php
/**
* Z-Push configuration utility for Kolab accounts
*
* @version 0.1
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * Copyright (C) 2011, Kolab Systems AG
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_zpush extends rcube_plugin
{
public $task = 'settings';
public $urlbase;
private $rc;
private $ui;
private $cache;
private $devices;
private $folders;
private $folders_meta;
private $root_meta;
const ROOT_MAILBOX = 'INBOX';
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
const ACTIVESYNC_KEY = '/private/vendor/kolab/activesync';
/**
* Plugin initialization.
*/
public function init()
{
$this->rc = rcmail::get_instance();
$this->require_plugin('jqueryui');
$this->add_texts('localization/', true);
$this->include_script('kolab_zpush.js');
$this->register_action('plugin.zpushconfig', array($this, 'config_view'));
$this->register_action('plugin.zpushjson', array($this, 'json_command'));
if ($this->rc->action == 'plugin.zpushconfig')
$this->require_plugin('kolab_core');
}
/**
* Establish IMAP connection
*/
public function init_imap()
{
$this->rc->imap_connect();
$this->cache = $this->rc->get_cache('zpush', 'db', 900);
$this->cache->expunge();
if ($meta = $this->rc->imap->get_metadata(self::ROOT_MAILBOX, self::ACTIVESYNC_KEY)) {
// clear cache if device config changed
if (($oldmeta = $this->cache->read('devicemeta')) && $oldmeta != $meta)
$this->cache->remove();
$this->root_meta = $this->unserialize_metadata($meta[self::ROOT_MAILBOX][self::ACTIVESYNC_KEY]);
$this->cache->remove('devicemeta');
$this->cache->write('devicemeta', $meta);
}
}
/**
* Handle JSON requests
*/
public function json_command()
{
$cmd = get_input_value('cmd', RCUBE_INPUT_GPC);
$imei = get_input_value('id', RCUBE_INPUT_GPC);
switch ($cmd) {
case 'load':
$result = array();
$this->init_imap();
$devices = $this->list_devices();
if ($device = $devices[$imei]) {
$result['id'] = $imei;
$result['devicealias'] = $device['ALIAS'];
$result['syncmode'] = intval($device['MODE']);
$result['laxpic'] = intval($device['LAXPIC']);
$result['subscribed'] = array();
foreach ($this->folders_meta() as $folder => $meta) {
if ($meta[$imei]['S'])
$result['subscribed'][$folder] = intval($meta[$imei]['S']);
}
$this->rc->output->command('plugin.zpush_data_ready', $result);
}
else {
$this->rc->output->show_message($this->gettext('devicenotfound'), 'error');
}
break;
case 'save':
$this->init_imap();
$devices = $this->list_devices();
$syncmode = intval(get_input_value('syncmode', RCUBE_INPUT_POST));
$devicealias = get_input_value('devicealias', RCUBE_INPUT_POST);
$laxpic = intval(get_input_value('laxpic', RCUBE_INPUT_POST));
$subsciptions = get_input_value('subscribed', RCUBE_INPUT_POST);
$err = false;
if ($device = $devices[$imei]) {
// update device config if changed
if ($devicealias != $this->root_meta['DEVICE'][$imei]['ALIAS'] ||
$syncmode != $this->root_meta['DEVICE'][$imei]['MODE'] ||
$laxpic != $this->root_meta['DEVICE'][$imei]['LAXPIC'] ||
$subsciptions[self::ROOT_MAILBOX] != $this->root_meta['FOLDER'][$imei]['S']) {
$this->root_meta['DEVICE'][$imei]['MODE'] = $syncmode;
$this->root_meta['DEVICE'][$imei]['ALIAS'] = $devicealias;
$this->root_meta['DEVICE'][$imei]['LAXPIC'] = $laxpic;
$this->root_meta['FOLDER'][$imei]['S'] = intval($subsciptions[self::ROOT_MAILBOX]);
$err = !$this->rc->imap->set_metadata(self::ROOT_MAILBOX,
array(self::ACTIVESYNC_KEY => $this->serialize_metadata($this->root_meta)));
// update cached meta data
if (!$err) {
$this->cache->remove('devicemeta');
$this->cache->write('devicemeta', $this->rc->imap->get_metadata(self::ROOT_MAILBOX, self::ACTIVESYNC_KEY));
}
}
// iterate over folders list and update metadata if necessary
foreach ($this->folders_meta() as $folder => $meta) {
// skip root folder (already handled above)
if ($folder == self::ROOT_MAILBOX)
continue;
if ($subsciptions[$folder] != $meta[$imei]['S']) {
$meta[$imei]['S'] = intval($subsciptions[$folder]);
$this->folders_meta[$folder] = $meta;
unset($meta['TYPE']);
// read metadata first
$folderdata = $this->rc->imap->get_metadata($folder, array(self::ACTIVESYNC_KEY));
if ($asyncdata = $folderdata[$folder][self::ACTIVESYNC_KEY])
$metadata = $this->unserialize_metadata($asyncdata);
$metadata['FOLDER'] = $meta;
$err |= !$this->rc->imap->set_metadata($folder, array(self::ACTIVESYNC_KEY => $this->serialize_metadata($metadata)));
}
}
// update cache
$this->cache->remove('folders');
$this->cache->write('folders', $this->folders_meta);
$this->rc->output->command('plugin.zpush_save_complete', array('success' => !$err, 'id' => $imei, 'devicealias' => Q($devicealias)));
}
if ($err)
$this->rc->output->show_message($this->gettext('savingerror'), 'error');
else
$this->rc->output->show_message($this->gettext('successfullysaved'), 'confirmation');
break;
case 'delete':
$this->init_imap();
$devices = $this->list_devices();
if ($device = $devices[$imei]) {
unset($this->root_meta['DEVICE'][$imei], $this->root_meta['FOLDER'][$imei]);
// update annotation and cached meta data
if ($success = $this->rc->imap->set_metadata(self::ROOT_MAILBOX, array(self::ACTIVESYNC_KEY => $this->serialize_metadata($this->root_meta)))) {
$this->cache->remove('devicemeta');
$this->cache->write('devicemeta', $this->rc->imap->get_metadata(self::ROOT_MAILBOX, self::ACTIVESYNC_KEY));
// remove device annotation in every folder
foreach ($this->folders_meta() as $folder => $meta) {
// skip root folder (already handled above)
if ($folder == self::ROOT_MAILBOX)
continue;
if (isset($meta[$imei])) {
$type = $meta['TYPE']; // remember folder type
unset($meta[$imei], $meta['TYPE']);
// read metadata first and update FOLDER property
$folderdata = $this->rc->imap->get_metadata($folder, array(self::ACTIVESYNC_KEY));
if ($asyncdata = $folderdata[$folder][self::ACTIVESYNC_KEY])
$metadata = $this->unserialize_metadata($asyncdata);
$metadata['FOLDER'] = $meta;
if ($this->rc->imap->set_metadata($folder, array(self::ACTIVESYNC_KEY => $this->serialize_metadata($metadata)))) {
$this->folders_meta[$folder] = $metadata;
$this->folders_meta[$folder]['TYPE'] = $type;
}
}
}
// update cache
$this->cache->remove('folders');
$this->cache->write('folders', $this->folders_meta);
}
}
if ($success) {
$this->rc->output->show_message($this->gettext('successfullydeleted'), 'confirmation');
$this->rc->output->redirect(array('action' => 'plugin.zpushconfig')); // reload UI
}
else
$this->rc->output->show_message($this->gettext('savingerror'), 'error');
break;
}
$this->rc->output->send();
}
/**
* Render main UI for device configuration
*/
public function config_view()
{
require_once($this->home . '/kolab_zpush_ui.php');
$this->init_imap();
// checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
if (!($this->rc->imap->get_capability('METADATA') || $this->rc->imap->get_capability('ANNOTATEMORE') || $this->rc->imap->get_capability('ANNOTATEMORE2'))) {
$this->rc->output->show_message($this->gettext('notsupported'), 'error');
}
$this->ui = new kolab_zpush_ui($this);
$this->register_handler('plugin.devicelist', array($this->ui, 'device_list'));
$this->register_handler('plugin.deviceconfigform', array($this->ui, 'device_config_form'));
$this->register_handler('plugin.foldersubscriptions', array($this->ui, 'folder_subscriptions'));
$this->rc->output->set_env('devicecount', count($this->list_devices()));
$this->rc->output->send('kolab_zpush.config');
}
/**
* List known devices
*
* @return array Device list as hash array
*/
public function list_devices()
{
if (!isset($this->devices)) {
$this->devices = (array)$this->root_meta['DEVICE'];
}
return $this->devices;
}
/**
* Get list of all folders available for sync
*
* @return array List of mailbox folders
*/
public function list_folders()
{
if (!isset($this->folders)) {
// read cached folder meta data
if ($cached_folders = $this->cache->read('folders')) {
$this->folders_meta = $cached_folders;
$this->folders = array_keys($this->folders_meta);
}
// fetch folder data from server
else {
$this->folders = $this->rc->imap->list_unsubscribed();
foreach ($this->folders as $folder) {
$folderdata = $this->rc->imap->get_metadata($folder, array(self::ACTIVESYNC_KEY, self::CTYPE_KEY));
$foldertype = explode('.', $folderdata[$folder][self::CTYPE_KEY]);
if ($asyncdata = $folderdata[$folder][self::ACTIVESYNC_KEY]) {
$metadata = $this->unserialize_metadata($asyncdata);
$this->folders_meta[$folder] = $metadata['FOLDER'];
}
$this->folders_meta[$folder]['TYPE'] = !empty($foldertype[0]) ? $foldertype[0] : 'mail';
}
// cache it!
$this->cache->write('folders', $this->folders_meta);
}
}
return $this->folders;
}
/**
* Getter for folder metadata
*
* @return array Hash array with meta data for each folder
*/
public function folders_meta()
{
if (!isset($this->folders_meta))
$this->list_folders();
return $this->folders_meta;
}
/**
* Helper method to decode saved IMAP metadata
*/
private function unserialize_metadata($str)
{
if (!empty($str))
return @json_decode(base64_decode($str), true);
return null;
}
/**
* Helper method to encode IMAP metadata for saving
*/
private function serialize_metadata($data)
{
if (is_array($data))
return base64_encode(json_encode($data));
return '';
}
-}
\ No newline at end of file
+}
diff --git a/plugins/kolab_zpush/kolab_zpush_ui.php b/plugins/kolab_zpush/kolab_zpush_ui.php
index 02fcfda9..559cb5eb 100644
--- a/plugins/kolab_zpush/kolab_zpush_ui.php
+++ b/plugins/kolab_zpush/kolab_zpush_ui.php
@@ -1,217 +1,215 @@
<?php
/**
* Z-Push configuration user interface builder
*
* @version 0.1
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * Copyright (C) 2011, Kolab Systems AG
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_zpush_ui
{
private $rc;
private $config;
public function __construct($config)
{
$this->config = $config;
$this->rc = rcmail::get_instance();
$skin = $this->rc->config->get('skin');
$this->config->include_stylesheet('skins/' . $skin . '/config.css');
$this->rc->output->include_script('list.js');
$this->skin_path = $this->config->urlbase . 'skins/' . $skin . '/';
}
public function device_list($attrib = array())
{
$attrib += array('id' => 'devices-list');
$devices = $this->config->list_devices();
$table = new html_table();
foreach ($devices as $id => $device) {
$name = $device['ALIAS'] ? $device['ALIAS'] : $id;
$table->add_row(array('id' => 'rcmrow' . $id));
$table->add(null, html::span('devicealias', Q($name)) . html::span('devicetype', Q($device['TYPE'])));
}
$this->rc->output->add_gui_object('devicelist', $attrib['id']);
$this->rc->output->set_env('devices', $devices);
return $table->show($attrib);
}
public function device_config_form($attrib = array())
{
$table = new html_table(array('cols' => 2));
$field_id = 'config-device-alias';
$input = new html_inputfield(array('name' => 'devicealias', 'id' => $field_id, 'size' => 40));
$table->add('title', html::label($field_id, $this->config->gettext('devicealias')));
$table->add(null, $input->show());
$field_id = 'config-device-mode';
$select = new html_select(array('name' => 'syncmode', 'id' => $field_id));
$select->add(array($this->config->gettext('modeauto'), $this->config->gettext('modeflat'), $this->config->gettext('modefolder')), array('-1', '0', '1'));
$table->add('title', html::label($field_id, $this->config->gettext('syncmode')));
$table->add(null, $select->show('-1'));
$field_id = 'config-device-laxpic';
$checkbox = new html_checkbox(array('name' => 'laxpic', 'value' => '1', 'id' => $field_id));
$table->add('title', $this->config->gettext('imageformat'));
$table->add(null, html::label($field_id, $checkbox->show() . ' ' . $this->config->gettext('laxpiclabel')));
if ($attrib['form'])
$this->rc->output->add_gui_object('editform', $attrib['form']);
return $table->show($attrib);
}
public function folder_subscriptions($attrib = array())
{
if (!$attrib['id'])
$attrib['id'] = 'foldersubscriptions';
// group folders by type
$folder_groups = array('mail' => array(), 'contact' => array(), 'event' => array());
$folder_meta = $this->config->folders_meta();
foreach ($this->config->list_folders() as $folder) {
$type = $folder_meta[$folder]['TYPE'] ? $folder_meta[$folder]['TYPE'] : 'mail';
$folder_groups[$type][] = $folder;
}
// build block for every folder type
foreach ($folder_groups as $type => $group) {
if (empty($group))
continue;
$attrib['type'] = $type;
$html .= html::div('subscriptionblock',
html::tag('h3', $type, $this->config->gettext($type)) .
$this->folder_subscriptions_block($group, $attrib));
}
$this->rc->output->add_gui_object('subscriptionslist', $attrib['id']);
return html::div($attrib, $html);
}
public function folder_subscriptions_block($a_folders, $attrib)
{
$alarms = ($attrib['type'] == 'event' || $attrib['type'] == 'task');
$table = new html_table(array('cellspacing' => 0));
$table->add_header('subscription', $attrib['syncicon'] ? html::img(array('src' => $this->skin_path . $attrib['syncicon'], 'title' => $this->config->gettext('synchronize'))) : '');
$table->add_header('alarm', $alarms && $attrib['alarmicon'] ? html::img(array('src' => $this->skin_path . $attrib['alarmicon'], 'title' => $this->config->gettext('withalarms'))) : '');
$table->add_header('foldername', $this->config->gettext('folder'));
$checkbox_sync = new html_checkbox(array('name' => 'subscribed[]', 'class' => 'subscription'));
$checkbox_alarm = new html_checkbox(array('name' => 'alarm[]', 'class' => 'alarm', 'disabled' => true));
$names = array();
foreach ($a_folders as $folder) {
$foldername = $origname = preg_replace('/^INBOX &raquo;\s+/', '', rcube_kolab::object_name($folder));
// find folder prefix to truncate (the same code as in kolab_addressbook plugin)
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($foldername, $names[$i].' &raquo; ') === 0) {
$length = strlen($names[$i].' &raquo; ');
$prefix = substr($foldername, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$foldername = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($foldername, $length);
break;
}
}
$names[] = $origname;
$classes = array('mailbox');
if ($folder_class = rcmail_folder_classname($folder)) {
$foldername = rcube_label($folder_class);
$classes[] = $folder_class;
}
$folder_id = 'rcmf' . html_identifier($folder);
$padding = str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;', $level);
$table->add_row(array('class' => (($level+1) * $idx++) % 2 == 0 ? 'even' : 'odd'));
$table->add('subscription', $checkbox_sync->show('', array('value' => $folder, 'id' => $folder_id)));
if ($alarms)
$table->add('alarm', $checkbox_alarm->show('', array('value' => $folder, 'id' => $folder_id.'_alarm')));
else
$table->add('alarm', '');
$table->add(join(' ', $classes), html::label($folder_id, $padding . Q($foldername)));
}
return $table->show();
}
/**
* Recursively compose folders table
*/
private function render_folders($a_folders, $table, $level = 0)
{
$idx = 0;
$checkbox_sync = new html_checkbox(array('name' => 'subscribed[]', 'class' => 'subscription'));
$checkbox_alarm = new html_checkbox(array('name' => 'alarm[]', 'class' => 'alarm', 'disabled' => true));
$folders_meta = $this->config->folders_meta();
foreach ($a_folders as $key => $folder) {
$classes = array('mailbox');
if ($folder_class = rcmail_folder_classname($folder['id'])) {
$foldername = rcube_label($folder_class);
$classes[] = $folder_class;
}
else
$foldername = $folder['name'];
// visualize folder type
if ($type = $folders_meta[$folder['id']]['TYPE'])
$classes[] = $type;
if ($folder['virtual'])
$classes[] = 'virtual';
$folder_id = 'rcmf' . html_identifier($folder['id']);
$padding = str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;', $level);
$table->add_row(array('class' => (($level+1) * $idx++) % 2 == 0 ? 'even' : 'odd'));
$table->add(join(' ', $classes), html::label($folder_id, $padding . Q($foldername)));
$table->add('subscription', $folder['virtual'] ? '' : $checkbox_sync->show('', array('value' => $folder['id'], 'id' => $folder_id)));
if (($type == 'event' || $type == 'task') && !$folder['virtual'])
$table->add('alarm', $checkbox_alarm->show('', array('value' => $folder['id'], 'id' => $folder_id.'_alarm')));
else
$table->add('alarm', '');
if (!empty($folder['folders']))
$this->render_folders($folder['folders'], $table, $level+1);
}
}
}
-
diff --git a/plugins/ldap_authentication/LICENSE b/plugins/ldap_authentication/LICENSE
new file mode 100644
index 00000000..dba13ed2
--- /dev/null
+++ b/plugins/ldap_authentication/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, 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
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state 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 program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/plugins/ldap_authentication/ldap_authentication.php b/plugins/ldap_authentication/ldap_authentication.php
index d491a8c3..3d7c7b6d 100644
--- a/plugins/ldap_authentication/ldap_authentication.php
+++ b/plugins/ldap_authentication/ldap_authentication.php
@@ -1,140 +1,140 @@
<?php
/**
* LDAP Authentication
*
* Authenticate on LDAP server, finds canonized authentication ID for IMAP
* and for new users create identity based on LDAP information.
*
* @version 0.1
* @author Aleksander Machniak <machniak@kolabsys.com>
*
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2
- * as published by the Free Software Foundation.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
+ * GNU Affero General Public License for more details.
*
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class ldap_authentication extends rcube_plugin
{
public $task = 'login';
private $ldap;
private $data = array();
function init()
{
$this->add_hook('authenticate', array($this, 'authenticate'));
$this->add_hook('user_create', array($this, 'user_create'));
}
function user_create($args)
{
if (!empty($this->data['user_email']))
$args['user_email'] = $this->data['user_email'];
if (!empty($this->data['user_name']))
$args['user_name'] = $this->data['user_name'];
if (!empty($this->data['user_alias']))
$args['user_alias'] = $this->data['user_alias'];
return $args;
}
function authenticate($args)
{
if ($this->init_ldap()) {
$rcmail = rcmail::get_instance();
$filter = $rcmail->config->get('ldap_authentication_filter');
$domain = $rcmail->config->get('username_domain');
// get username and host
$user = $args['user'];
$host = rcube_parse_host($args['host']);
if (!empty($domain) && strpos($user, '@') === false) {
if (is_array($domain) && isset($domain[$args['host']]))
$user .= '@'.rcube_parse_host($domain[$host], $host);
else if (is_string($domain))
$user .= '@'.rcube_parse_host($domain, $host);
}
// replace variables in filter
list($u, $d) = explode('@', $user);
$dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
$replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u);
$filter = strtr($filter, $replaces);
// get record
$this->ldap->set_filter($filter);
$results = $this->ldap->list_records();
if (count($results->records) == 1) {
$record = $results->records[0];
$login_attr = $rcmail->config->get('ldap_authentication_login');
$alias_attr = $rcmail->config->get('ldap_authentication_alias');
$name_attr = $rcmail->config->get('ldap_authentication_name');
if ($login_attr)
$this->data['user_login'] = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
if ($alias_attr)
$this->data['user_alias'] = is_array($record[$alias_attr]) ? $record[$alias_attr][0] : $record[$alias_attr];
if ($name_attr)
$this->data['user_name'] = is_array($record[$name_attr]) ? $record[$name_attr][0] : $record[$name_attr];
if ($this->data['user_login'])
$args['user'] = $this->data['user_login'];
}
}
return $args;
}
private function init_ldap()
{
if ($this->ldap)
return $this->ldap->ready;
$this->load_config();
$rcmail = rcmail::get_instance();
$addressbook = $rcmail->config->get('ldap_authentication_addressbook');
if (!is_array($addressbook)) {
$ldap_config = (array)$rcmail->config->get('ldap_public');
$addressbook = $ldap_config[$addressbook];
}
if (empty($addressbook)) {
return false;
}
$this->ldap = new ldap_authentication_ldap_backend(
$addressbook,
$rcmail->config->get('ldap_debug'),
$rcmail->config->mail_domain($_SESSION['imap_host'])
);
return $this->ldap->ready;
}
}
class ldap_authentication_ldap_backend extends rcube_ldap
{
function set_filter($filter)
{
if ($filter)
$this->prop['filter'] = $filter;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Aug 17, 1:03 PM (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1261456
Default Alt Text
(860 KB)

Event Timeline