pax_global_header00006660000000000000000000000064146771706620014532gustar00rootroot0000000000000052 comment=c407f9e89d473d1a581a277ea0e1d6b83f997b0f buteo-sync-plugin-caldav-0.3.14/000077500000000000000000000000001467717066200164335ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/.gitignore000066400000000000000000000002301467717066200204160ustar00rootroot00000000000000# C++ objects and libs *.slo *.lo *.o *.a *.la *.lai *.so *.dll *.dylib # Qt-es *.pro.user *.pro.user.* moc_*.cpp *.moc qrc_*.cpp Makefile *-build-* buteo-sync-plugin-caldav-0.3.14/LICENSE000066400000000000000000000643121467717066200174460ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE buteo-sync-plugin-caldav Copyright (C) 2014 Jolla Ltd. Contact: info@jolla.com You may use, distribute and copy the Nemo Transfer Engine under the terms of GNU Lesser General Public License version 2.1. ------------------------------------------------------------------------- GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation and appearing in the file license.lgpl included in the packaging of this file. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! buteo-sync-plugin-caldav-0.3.14/README000066400000000000000000000004351467717066200173150ustar00rootroot00000000000000This package provides a "client" synchronization plugin to the Buteo sync framework. It connects to CalDAV servers using a valid account (accounts&sso framework) and synchronizes calendar data from the remote CalDAV service to the device and vice versa. It is a two-way sync plugin. buteo-sync-plugin-caldav-0.3.14/README.md000066400000000000000000000001141467717066200177060ustar00rootroot00000000000000buteo-sync-plugin-caldav ======================== CalDav Plugin for Buteo buteo-sync-plugin-caldav-0.3.14/buteo-sync-plugin-caldav.pro000066400000000000000000000003071467717066200237710ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = src tests mkcal tests.depends = src OTHER_FILES += rpm/buteo-sync-plugin-caldav.spec \ src/xmls/client/caldav.xml \ src/xmls/sync/caldav-sync.xml buteo-sync-plugin-caldav-0.3.14/mkcal/000077500000000000000000000000001467717066200175225ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/mkcal/caldavinvitationplugin.cpp000066400000000000000000000101341467717066200250030ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2019 - 2020 Open Mobile Platform LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "caldavinvitationplugin.h" #include #include #include using namespace KCalendarCore; class CalDAVInvitationPluginPrivate { public: ServiceInterface::ErrorCode m_errorCode = ServiceInterface::ErrorNotSupported; }; CalDAVInvitationPlugin::CalDAVInvitationPlugin() : d(new CalDAVInvitationPluginPrivate) { } CalDAVInvitationPlugin::~CalDAVInvitationPlugin() { delete d; } bool CalDAVInvitationPlugin::sendInvitation( const QString &, const QString &, const Incidence::Ptr &, const QString &) { d->m_errorCode = ServiceInterface::ErrorNotSupported; return false; } bool CalDAVInvitationPlugin::sendUpdate( const QString &, const Incidence::Ptr &, const QString &) { d->m_errorCode = ServiceInterface::ErrorNotSupported; return false; } bool CalDAVInvitationPlugin::sendResponse( const QString &, const Incidence::Ptr &, const QString &) { d->m_errorCode = ServiceInterface::ErrorNotSupported; return false; } QString CalDAVInvitationPlugin::pluginName() const { d->m_errorCode = ServiceInterface::ErrorOk; return QLatin1String("caldav"); } QString CalDAVInvitationPlugin::uiName() const { d->m_errorCode = ServiceInterface::ErrorOk; return QLatin1String("CalDAV"); } QString CalDAVInvitationPlugin::icon() const { d->m_errorCode = ServiceInterface::ErrorOk; return QString(); } bool CalDAVInvitationPlugin::multiCalendar() const { d->m_errorCode = ServiceInterface::ErrorNotSupported; return false; } static const QByteArray EMAIL_PROPERTY = QByteArrayLiteral("userPrincipalEmail"); QString CalDAVInvitationPlugin::emailAddress( const mKCal::Notebook::Ptr ¬ebook) { d->m_errorCode = ServiceInterface::ErrorOk; return notebook->customProperty(EMAIL_PROPERTY); } QString CalDAVInvitationPlugin::displayName( const mKCal::Notebook::Ptr &) const { d->m_errorCode = ServiceInterface::ErrorNotSupported; return QString(); } bool CalDAVInvitationPlugin::downloadAttachment( const mKCal::Notebook::Ptr &, const QString &, const QString &) { d->m_errorCode = ServiceInterface::ErrorNotSupported; return false; } bool CalDAVInvitationPlugin::deleteAttachment( const mKCal::Notebook::Ptr &, const Incidence::Ptr &, const QString &) { d->m_errorCode = ServiceInterface::ErrorNotSupported; return false; } bool CalDAVInvitationPlugin::shareNotebook( const mKCal::Notebook::Ptr &, const QStringList &) { d->m_errorCode = ServiceInterface::ErrorNotSupported; return false; } QStringList CalDAVInvitationPlugin::sharedWith( const mKCal::Notebook::Ptr &) { d->m_errorCode = ServiceInterface::ErrorNotSupported; return QStringList(); } QString CalDAVInvitationPlugin::CalDAVInvitationPlugin::serviceName() const { return pluginName(); } QString CalDAVInvitationPlugin::defaultNotebook() const { d->m_errorCode = ServiceInterface::ErrorNotSupported; return QString(); } bool CalDAVInvitationPlugin::checkProductId( const QString &) const { d->m_errorCode = ServiceInterface::ErrorNotSupported; return false; } ServiceInterface::ErrorCode CalDAVInvitationPlugin::error() const { return d->m_errorCode; } buteo-sync-plugin-caldav-0.3.14/mkcal/caldavinvitationplugin.h000066400000000000000000000061011467717066200244470ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2019 - 2020 Open Mobile Platform LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CALDAVINVITATIONPLUGIN_H #define CALDAVINVITATIONPLUGIN_H // KCalendarCore #include // mKCal #include #include using namespace KCalendarCore; class CalDAVInvitationPluginPrivate; class CalDAVInvitationPlugin : public QObject, public InvitationHandlerInterface, public ServiceInterface { Q_OBJECT Q_INTERFACES(InvitationHandlerInterface) Q_INTERFACES(ServiceInterface) Q_PLUGIN_METADATA(IID "org.qt-project.Qt.mkcal.CalDAVInvitationHandlerInterface") public: CalDAVInvitationPlugin(); ~CalDAVInvitationPlugin(); // InvitationHandler bool sendInvitation(const QString &accountId, const QString ¬ebookId, const Incidence::Ptr &invitation, const QString &body) override; bool sendUpdate(const QString &accountId, const Incidence::Ptr &invitation, const QString &body) override; bool sendResponse(const QString &accountId, const Incidence::Ptr &invitation, const QString &body) override; QString pluginName() const override; // ServiceHandler QString uiName() const override; QString icon() const override; bool multiCalendar() const override; QString emailAddress(const mKCal::Notebook::Ptr ¬ebook) override; QString displayName(const mKCal::Notebook::Ptr ¬ebook) const override; bool downloadAttachment(const mKCal::Notebook::Ptr ¬ebook, const QString &uri, const QString &path) override; bool deleteAttachment(const mKCal::Notebook::Ptr ¬ebook, const Incidence::Ptr &incidence, const QString &uri) override; bool shareNotebook(const mKCal::Notebook::Ptr ¬ebook, const QStringList &sharedWith) override; QStringList sharedWith(const mKCal::Notebook::Ptr ¬ebook) override; QString serviceName() const override; QString defaultNotebook() const override; bool checkProductId(const QString &productId) const override; ErrorCode error() const override; private: Q_DISABLE_COPY(CalDAVInvitationPlugin) CalDAVInvitationPluginPrivate * const d; }; #endif buteo-sync-plugin-caldav-0.3.14/mkcal/mkcal.pro000066400000000000000000000005351467717066200213360ustar00rootroot00000000000000TEMPLATE = lib TARGET = $$qtLibraryTarget(caldavinvitationplugin) CONFIG += plugin CONFIG += link_pkgconfig PKGCONFIG += \ QmfClient \ KF5CalendarCore \ libmkcal-qt5 QT -= gui HEADERS += caldavinvitationplugin.h SOURCES += caldavinvitationplugin.cpp target.path += /${DESTDIR}$$[QT_INSTALL_LIBS]/mkcalplugins/ INSTALLS += target buteo-sync-plugin-caldav-0.3.14/rpm/000077500000000000000000000000001467717066200172315ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/rpm/buteo-sync-plugin-caldav.spec000066400000000000000000000060421467717066200247230ustar00rootroot00000000000000Name: buteo-sync-plugin-caldav Summary: Syncs calendar data from CalDAV services Version: 0.3.13 Release: 1 License: LGPLv2 URL: https://github.com/sailfishos/buteo-sync-plugin-caldav/ Source0: %{name}-%{version}.tar.bz2 BuildRequires: pkgconfig(Qt5Core) BuildRequires: pkgconfig(Qt5DBus) BuildRequires: pkgconfig(Qt5Network) BuildRequires: pkgconfig(Qt5Test) BuildRequires: pkgconfig(libsignon-qt5) BuildRequires: pkgconfig(libsailfishkeyprovider) BuildRequires: pkgconfig(libmkcal-qt5) >= 0.7.17 BuildRequires: pkgconfig(KF5CalendarCore) >= 5.79 BuildRequires: pkgconfig(buteosyncfw5) >= 0.10.11 BuildRequires: pkgconfig(accounts-qt5) BuildRequires: pkgconfig(signon-oauth2plugin) BuildRequires: pkgconfig(QmfClient) Requires: buteo-syncfw-qt5-msyncd %description A Buteo plugin which syncs calendar data from CalDAV services %package tests Summary: Unit tests for buteo-sync-plugin-caldav Requires: blts-tools Requires: %{name} = %{version}-%{release} %description tests This package contains unit tests for the CalDAV Buteo sync plugin. %prep %setup -q -n %{name}-%{version} %build %qmake5 %make_build %install %qmake5_install %files %license LICENSE %config %{_sysconfdir}/buteo/profiles/client/caldav.xml %config %{_sysconfdir}/buteo/profiles/sync/caldav-sync.xml %{_libdir}/buteo-plugins-qt5/oopp/libcaldav-client.so #mkcal invitation plugin %{_libdir}/mkcalplugins/libcaldavinvitationplugin.so %files tests /opt/tests/buteo/plugins/caldav/tests.xml /opt/tests/buteo/plugins/caldav/tst_reader /opt/tests/buteo/plugins/caldav/tst_notebooksyncagent /opt/tests/buteo/plugins/caldav/tst_propfind /opt/tests/buteo/plugins/caldav/tst_caldavclient /opt/tests/buteo/plugins/caldav/data/notebooksyncagent_insert_exdate.xml /opt/tests/buteo/plugins/caldav/data/notebooksyncagent_insert_and_update.xml /opt/tests/buteo/plugins/caldav/data/notebooksyncagent_recurring.xml /opt/tests/buteo/plugins/caldav/data/notebooksyncagent_simple.xml /opt/tests/buteo/plugins/caldav/data/notebooksyncagent_orphanexceptions.xml /opt/tests/buteo/plugins/caldav/data/reader_CR_description.xml /opt/tests/buteo/plugins/caldav/data/reader_UID.xml /opt/tests/buteo/plugins/caldav/data/reader_earlyUID.xml /opt/tests/buteo/plugins/caldav/data/reader_UTF8_description.xml /opt/tests/buteo/plugins/caldav/data/reader_base.xml /opt/tests/buteo/plugins/caldav/data/reader_noevent.xml /opt/tests/buteo/plugins/caldav/data/reader_missing.xml /opt/tests/buteo/plugins/caldav/data/reader_basic_vcal.xml /opt/tests/buteo/plugins/caldav/data/reader_noxml.xml /opt/tests/buteo/plugins/caldav/data/reader_nodav.xml /opt/tests/buteo/plugins/caldav/data/reader_fullday.xml /opt/tests/buteo/plugins/caldav/data/reader_fullday_vcal.xml /opt/tests/buteo/plugins/caldav/data/reader_xmltag.xml /opt/tests/buteo/plugins/caldav/data/reader_urldescription.xml /opt/tests/buteo/plugins/caldav/data/reader_relativealarm.xml /opt/tests/buteo/plugins/caldav/data/reader_cdata.xml /opt/tests/buteo/plugins/caldav/data/reader_todo_pending.xml /opt/tests/buteo/plugins/caldav/data/reader_unexpected_elements.xml buteo-sync-plugin-caldav-0.3.14/src/000077500000000000000000000000001467717066200172225ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/src/authhandler.cpp000066400000000000000000000135001467717066200222240ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "authhandler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "logging.h" #include using namespace Accounts; using namespace SignOn; const QString RESPONSE_TYPE ("ResponseType"); const QString SCOPE ("Scope"); const QString AUTH_PATH ("AuthPath"); const QString TOKEN_PATH ("TokenPath"); const QString REDIRECT_URI ("RedirectUri"); const QString HOST ("Host"); AuthHandler::AuthHandler(QSharedPointer service, QObject *parent) : QObject(parent) , mAccountService(service) { } bool AuthHandler::init() { FUNCTION_CALL_TRACE(lcCalDavTrace); if (mAccountService == NULL) { qCDebug(lcCalDav) << "Invalid account service"; return false; } const Accounts::AuthData &auth = mAccountService->authData(); if (auth.credentialsId() == 0) { qCWarning(lcCalDav) << "Cannot authenticate, no credentials stored for service:" << mAccountService->service().name(); return false; } mIdentity = Identity::existingIdentity(auth.credentialsId(), this); if (!mIdentity) { qCWarning(lcCalDav) << "Cannot get existing identity for credentials:" << auth.credentialsId(); return false; } mSession = mIdentity->createSession(auth.method().toLatin1()); if (!mSession) { qCDebug(lcCalDav) << "Signon session could not be created with method" << auth.method(); return false; } connect(mSession, SIGNAL(response(const SignOn::SessionData &)), this, SLOT(sessionResponse(const SignOn::SessionData &))); connect(mSession, SIGNAL(error(const SignOn::Error &)), this, SLOT(error(const SignOn::Error &))); return true; } void AuthHandler::sessionResponse(const SessionData &sessionData) { FUNCTION_CALL_TRACE(lcCalDavTrace); if (mSession->name().compare("password", Qt::CaseInsensitive) == 0) { mUsername = sessionData.UserName(); mPassword = sessionData.Secret(); } else if (mSession->name().compare("oauth2", Qt::CaseInsensitive) == 0) { OAuth2PluginNS::OAuth2PluginTokenData response = sessionData.data(); mToken = response.AccessToken(); } else { qCCritical(lcCalDav) << "Unsupported Mechanism requested!"; emit failed(); return; } qCDebug(lcCalDav) << "Authenticated!"; emit success(); } const QString AuthHandler::token() { return mToken; } const QString AuthHandler::username() { return mUsername; } const QString AuthHandler::password() { return mPassword; } void AuthHandler::authenticate() { FUNCTION_CALL_TRACE(lcCalDavTrace); const Accounts::AuthData &auth = mAccountService->authData(); if (mSession->name().compare("password", Qt::CaseInsensitive) == 0) { SignOn::SessionData data(auth.parameters()); data.setUiPolicy(SignOn::NoUserInteractionPolicy); mSession->process(data, auth.mechanism()); } else if (mSession->name().compare("oauth2", Qt::CaseInsensitive) == 0) { const QByteArray providerName = mAccountService->account()->providerName().toLatin1(); const QString clientId = storedKeyValue(providerName.constData(), "caldav", "client_id"); const QString clientSecret = storedKeyValue(providerName.constData(), "caldav", "client_secret"); OAuth2PluginNS::OAuth2PluginData data; data.setClientId(clientId); data.setClientSecret(clientSecret); data.setHost(auth.parameters().value(HOST).toString()); data.setAuthPath(auth.parameters().value(AUTH_PATH).toString()); data.setTokenPath(auth.parameters().value(TOKEN_PATH).toString()); data.setRedirectUri(auth.parameters().value(REDIRECT_URI).toString()); data.setResponseType(QStringList() << auth.parameters().value(RESPONSE_TYPE).toString()); data.setScope(auth.parameters().value(SCOPE).toStringList()); mSession->process(data, auth.mechanism()); } else { qCCritical(lcCalDav) << "Unsupported Method requested!"; emit failed(); } } void AuthHandler::error(const SignOn::Error & error) { FUNCTION_CALL_TRACE(lcCalDavTrace); qCDebug(lcCalDav) << "Auth error:" << error.message(); emit failed(); } QString AuthHandler::storedKeyValue(const char *provider, const char *service, const char *keyName) { FUNCTION_CALL_TRACE(lcCalDavTrace); char *storedKey = NULL; QString retn; int success = SailfishKeyProvider_storedKey(provider, service, keyName, &storedKey); if (success == 0 && storedKey != NULL && strlen(storedKey) != 0) { retn = QLatin1String(storedKey); } if (storedKey) { free(storedKey); } return retn; } buteo-sync-plugin-caldav-0.3.14/src/authhandler.h000066400000000000000000000040361467717066200216750ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef AUTHHANDLER_H #define AUTHHANDLER_H #include #include #include #include #include #include #include #include class AuthHandler : public QObject { Q_OBJECT public: explicit AuthHandler(QSharedPointer service, QObject *parent = 0); void authenticate(); const QString token(); bool init(); const QString username(); const QString password(); Q_SIGNALS: void success(); void failed(); private: void getToken(); void processTokenResponse(const QByteArray &tokenJSON); void deviceAuth(); void processDeviceCode(const QByteArray &deviceCodeJSON); QString storedKeyValue(const char *provider, const char *service, const char *keyName); private Q_SLOTS: void error(const SignOn::Error &); void sessionResponse(const SignOn::SessionData &); private: SignOn::Identity *mIdentity; SignOn::AuthSession *mSession; QSharedPointer mAccountService; QString mToken; QString mUsername; QString mPassword; }; #endif // AUTHHANDLER_H buteo-sync-plugin-caldav-0.3.14/src/buteo-caldav-plugin.h000066400000000000000000000021771467717066200232440ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef BUTEOCALDAVPLUGIN_H #define BUTEOCALDAVPLUGIN_H #include #if defined(BUTEOCALDAVPLUGIN_LIBRARY) # define BUTEOCALDAVPLUGINSHARED_EXPORT Q_DECL_EXPORT #else # define BUTEOCALDAVPLUGINSHARED_EXPORT Q_DECL_IMPORT #endif #endif // BUTEOCALDAVPLUGIN_GLOBAL_H buteo-sync-plugin-caldav-0.3.14/src/caldavclient.cpp000066400000000000000000000705321467717066200223660ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (c) 2013 - 2021 Jolla Ltd. and/or its subsidiary(-ies). * Copyright (c) 2020 Open Mobile Platform LLC. * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "caldavclient.h" #include "propfind.h" #include "notebooksyncagent.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "logging.h" #include #include namespace { const QString cleanSyncMarkersFileDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/system/privileged/Sync"); const QString cleanSyncMarkersFile = cleanSyncMarkersFileDir + QStringLiteral("/caldav.ini"); const char * const SYNC_PREV_PERIOD_KEY = "Sync Previous Months Span"; const char * const SYNC_NEXT_PERIOD_KEY = "Sync Next Months Span"; } Buteo::ClientPlugin* CalDavClientLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new CalDavClient(pluginName, profile, cbInterface); } CalDavClient::CalDavClient(const QString& aPluginName, const Buteo::SyncProfile& aProfile, Buteo::PluginCbInterface *aCbInterface) : ClientPlugin(aPluginName, aProfile, aCbInterface) , mManager(0) , mAuth(0) , mCalendar(0) , mStorage(0) { FUNCTION_CALL_TRACE(lcCalDavTrace); } CalDavClient::~CalDavClient() { FUNCTION_CALL_TRACE(lcCalDavTrace); } bool CalDavClient::init() { FUNCTION_CALL_TRACE(lcCalDavTrace); mNAManager = new QNetworkAccessManager(this); if (initConfig()) { return true; } else { // Uninitialize everything that was initialized before failure. uninit(); return false; } } bool CalDavClient::uninit() { FUNCTION_CALL_TRACE(lcCalDavTrace); return true; } bool CalDavClient::startSync() { FUNCTION_CALL_TRACE(lcCalDavTrace); if (!mAuth) return false; mAuth->authenticate(); qCDebug(lcCalDav) << "Init done. Continuing with sync"; return true; } void CalDavClient::abortSync(Sync::SyncStatus aStatus) { Q_UNUSED(aStatus); FUNCTION_CALL_TRACE(lcCalDavTrace); for (NotebookSyncAgent *agent: mNotebookSyncAgents) { disconnect(agent, &NotebookSyncAgent::finished, this, &CalDavClient::notebookSyncFinished); agent->abort(); } syncFinished(Buteo::SyncResults::ABORTED, QLatin1String("Sync aborted")); } bool CalDavClient::cleanUp() { FUNCTION_CALL_TRACE(lcCalDavTrace); // This function is called after the account has been deleted to allow the plugin to remove // all the notebooks associated with the account. QString accountIdString = iProfile.key(Buteo::KEY_ACCOUNT_ID); int accountId = accountIdString.toInt(); if (accountId == 0) { qCWarning(lcCalDav) << "profile does not specify" << Buteo::KEY_ACCOUNT_ID; return false; } mKCal::ExtendedCalendar::Ptr calendar = mKCal::ExtendedCalendar::Ptr(new mKCal::ExtendedCalendar(QTimeZone::utc())); mKCal::ExtendedStorage::Ptr storage = mKCal::ExtendedCalendar::defaultStorage(calendar); if (!storage->open()) { calendar->close(); qCWarning(lcCalDav) << "unable to open calendar storage"; return false; } deleteNotebooksForAccount(accountId, calendar, storage); storage->close(); calendar->close(); return true; } void CalDavClient::deleteNotebooksForAccount(int accountId, mKCal::ExtendedCalendar::Ptr, mKCal::ExtendedStorage::Ptr storage) { FUNCTION_CALL_TRACE(lcCalDavTrace); if (storage) { QString notebookAccountPrefix = QString::number(accountId) + "-"; // for historical reasons! QString accountIdStr = QString::number(accountId); const mKCal::Notebook::List notebookList = storage->notebooks(); qCDebug(lcCalDav) << "Total Number of Notebooks in device = " << notebookList.count(); int deletedCount = 0; for (mKCal::Notebook::Ptr notebook : notebookList) { if (notebook->account() == accountIdStr || notebook->account().startsWith(notebookAccountPrefix)) { if (storage->deleteNotebook(notebook)) { deletedCount++; } } } qCDebug(lcCalDav) << "Deleted" << deletedCount << "notebooks"; } } bool CalDavClient::cleanSyncRequired() { static const QByteArray iniFileDir = cleanSyncMarkersFileDir.toUtf8(); static const QByteArray iniFile = cleanSyncMarkersFile.toUtf8(); // multiple CalDavClient processes might be spawned (e.g. sync with different accounts) // so use a process mutex to ensure that only one will access the clean sync marker file at any time. if (!mProcessMutex) { mProcessMutex.reset(new Sailfish::KeyProvider::ProcessMutex(iniFile.constData())); } mProcessMutex->lock(); char *cleaned_value = SailfishKeyProvider_ini_read( iniFile.constData(), "General", QStringLiteral("%1-cleaned").arg(mService->account()->id()).toLatin1()); bool alreadyClean = cleaned_value != 0 && strncmp(cleaned_value, "true", 4) == 0; free(cleaned_value); if (!alreadyClean) { // first, delete any data associated with this account, so this sync will be a clean sync. qCWarning(lcCalDav) << "Deleting caldav notebooks associated with this account:" << mService->account()->id() << "due to clean sync"; deleteNotebooksForAccount(mService->account()->id(), mCalendar, mStorage); // now delete notebooks for non-existent accounts. qCWarning(lcCalDav) << "Deleting caldav notebooks associated with nonexistent accounts due to clean sync"; // a) find out which accounts are associated with each of our notebooks. QList notebookAccountIds; const mKCal::Notebook::List allNotebooks = mStorage->notebooks(); for (mKCal::Notebook::Ptr nb : allNotebooks) { QString nbAccount = nb->account(); if (!nbAccount.isEmpty() && nb->pluginName().contains(QStringLiteral("caldav"))) { // caldav notebook->account() values used to be like: "55-/user/calendars/someCalendar" int indexOfHyphen = nbAccount.indexOf('-'); if (indexOfHyphen > 0) { // this is an old caldav notebook which used "accountId-remoteServerPath" form nbAccount.chop(nbAccount.length() - indexOfHyphen); } bool ok = true; int notebookAccountId = nbAccount.toInt(&ok); if (!ok) { qCWarning(lcCalDav) << "notebook account value was strange:" << nb->account() << "->" << nbAccount << "->" << "not ok"; } else { qCWarning(lcCalDav) << "found account id:" << notebookAccountId << "for" << nb->account() << "->" << nbAccount; if (!notebookAccountIds.contains(notebookAccountId)) { notebookAccountIds.append(notebookAccountId); } } } } // b) find out if any of those accounts don't exist - if not, Accounts::AccountIdList accountIdList = mManager->accountList(); for (int notebookAccountId : const_cast&>(notebookAccountIds)) { if (!accountIdList.contains(notebookAccountId)) { qCWarning(lcCalDav) << "purging notebooks for deleted caldav account" << notebookAccountId; deleteNotebooksForAccount(notebookAccountId, mCalendar, mStorage); } } // mark this account as having been cleaned. if (SailfishKeyProvider_ini_write( iniFileDir.constData(), iniFile.constData(), "General", QStringLiteral("%1-cleaned").arg(mService->account()->id()).toLatin1(), "true") != 0) { qCWarning(lcCalDav) << "Failed to mark account as clean! Next sync will be unnecessarily cleaned also!"; } // finished; return true because this will be a clean sync. qCWarning(lcCalDav) << "Finished pre-sync cleanup with caldav account" << mService->account()->id(); mProcessMutex->unlock(); return true; } mProcessMutex->unlock(); return false; } void CalDavClient::connectivityStateChanged(Sync::ConnectivityType aType, bool aState) { FUNCTION_CALL_TRACE(lcCalDavTrace); qCDebug(lcCalDav) << "Received connectivity change event:" << aType << " changed to " << aState; if (aType == Sync::CONNECTIVITY_INTERNET && !aState) { // we lost connectivity during sync. abortSync(Sync::SYNC_CONNECTION_ERROR); } } struct CalendarSettings { public: CalendarSettings(const QSharedPointer &service) : paths(service->value("calendars").toStringList()) , displayNames(service->value("calendar_display_names").toStringList()) , colors(service->value("calendar_colors").toStringList()) , enabled(service->value("enabled_calendars").toStringList()) { if (enabled.count() > paths.count() || paths.count() != displayNames.count() || paths.count() != colors.count()) { qCWarning(lcCalDav) << "Bad calendar data for account" << service->account()->id(); paths.clear(); displayNames.clear(); colors.clear(); enabled.clear(); } }; // Constructs a list of CalendarInfo from value stored in settings. QList toCalendars() const { QList allCalendarInfo; for (int i = 0; i < paths.count(); i++) { allCalendarInfo << PropFind::CalendarInfo(paths[i], displayNames[i], colors[i]); } return allCalendarInfo; }; QList enabledCalendars(const QList &calendars) const { QList filteredCalendarInfo; for (const PropFind::CalendarInfo &info : calendars) { if (!enabled.contains(info.remotePath)) { continue; } filteredCalendarInfo << info; } return filteredCalendarInfo; }; void add(const PropFind::CalendarInfo &infos) { paths.append(infos.remotePath); enabled.append(infos.remotePath); displayNames.append(infos.displayName); colors.append(infos.color); }; bool update(const PropFind::CalendarInfo &infos, bool &modified) { int i = paths.indexOf(infos.remotePath); if (i < 0) { return false; } if (displayNames[i] != infos.displayName || colors[i] != infos.color) { displayNames[i] = infos.displayName; colors[i] = infos.color; modified = true; } return true; }; bool remove(const QString &path) { int at = paths.indexOf(path); if (at >= 0) { paths.removeAt(at); enabled.removeAll(path); displayNames.removeAt(at); colors.removeAt(at); } return (at >= 0); } void store(Accounts::Account *account, const Accounts::Service &srv) { account->selectService(srv); account->setValue("calendars", paths); account->setValue("enabled_calendars", enabled); account->setValue("calendar_display_names", displayNames); account->setValue("calendar_colors", colors); account->selectService(Accounts::Service()); account->syncAndBlock(); }; private: QStringList paths; QStringList displayNames; QStringList colors; QStringList enabled; }; QList CalDavClient::loadAccountCalendars() const { const struct CalendarSettings calendarSettings(mService); return calendarSettings.enabledCalendars(calendarSettings.toCalendars()); } QList CalDavClient::mergeAccountCalendars(const QList &calendars) const { struct CalendarSettings calendarSettings(mService); bool modified = false; for (QList::ConstIterator it = calendars.constBegin(); it != calendars.constEnd(); ++it) { if (!calendarSettings.update(*it, modified)) { qCDebug(lcCalDav) << "Found a new upstream calendar:" << it->remotePath << it->displayName; calendarSettings.add(*it); modified = true; } else { qCDebug(lcCalDav) << "Already existing calendar:" << it->remotePath << it->displayName << it->color; } } if (modified) { qCDebug(lcCalDav) << "Store modifications to calendar settings."; calendarSettings.store(mService->account(), mService->service()); } return calendarSettings.enabledCalendars(calendars); } void CalDavClient::removeAccountCalendars(const QStringList &paths) { struct CalendarSettings calendarSettings(mService); bool modified = false; for (QStringList::ConstIterator it = paths.constBegin(); it != paths.constEnd(); ++it) { if (calendarSettings.remove(*it)) { qCDebug(lcCalDav) << "Found a deleted upstream calendar:" << *it; modified = true; } } if (modified) { calendarSettings.store(mService->account(), mService->service()); } } bool CalDavClient::initConfig() { FUNCTION_CALL_TRACE(lcCalDavTrace); qCDebug(lcCalDav) << "Initiating config..."; if (!mManager) { mManager = new Accounts::Manager(this); } QString accountIdString = iProfile.key(Buteo::KEY_ACCOUNT_ID); bool accountIdOk = false; int accountId = accountIdString.toInt(&accountIdOk); if (!accountIdOk) { qCWarning(lcCalDav) << "no account id specified," << Buteo::KEY_ACCOUNT_ID << "not found in profile"; return false; } if (!mService) { Accounts::Account *account = mManager->account(accountId); if (!account) { qCWarning(lcCalDav) << "cannot find account" << accountId; return false; } if (!account->isEnabled()) { qCWarning(lcCalDav) << "Account" << accountId << "is disabled!"; return false; } for (const Accounts::Service &srv : account->enabledServices()) { if (srv.serviceType().toLower() == QStringLiteral("caldav")) { account->selectService(srv); if (account->value("caldav-sync/profile_id").toString() == getProfileName()) { mService = QSharedPointer(new Accounts::AccountService(account, srv)); break; } } } } if (!mService) { qCWarning(lcCalDav) << "cannot find enabled caldav service in account" << accountId; return false; } Accounts::AccountService global(mService->account(), Accounts::Service()); mSettings.setServerAddress(mService->value("server_address", global.value("server_address")).toString()); if (mSettings.serverAddress().isEmpty()) { qCWarning(lcCalDav) << "remote_address not found in service settings"; return false; } mSettings.setDavRootPath(mService->value("webdav_path", global.value("webdav_path")).toString()); mSettings.setIgnoreSSLErrors(mService->value("ignore_ssl_errors", global.value("ignore_ssl_errors")).toBool()); mAuth = new AuthHandler(mService, this); if (!mAuth->init()) { return false; } connect(mAuth, &AuthHandler::success, this, &CalDavClient::start); connect(mAuth, &AuthHandler::failed, this, &CalDavClient::authenticationError); mSyncDirection = iProfile.syncDirection(); mConflictResPolicy = iProfile.conflictResolutionPolicy(); return true; } void CalDavClient::syncFinished(Buteo::SyncResults::MinorCode minorErrorCode, const QString &message) { FUNCTION_CALL_TRACE(lcCalDavTrace); clearAgents(); if (mCalendar) { mCalendar->close(); } if (mStorage) { mStorage->close(); mStorage.clear(); } if (minorErrorCode == Buteo::SyncResults::NO_ERROR || minorErrorCode == Buteo::SyncResults::ITEM_FAILURES) { qCDebug(lcCalDav) << "CalDAV sync succeeded!" << message; mResults.setMajorCode(Buteo::SyncResults::SYNC_RESULT_SUCCESS); mResults.setMinorCode(minorErrorCode); emit success(getProfileName(), message); } else { qCWarning(lcCalDav) << "CalDAV sync failed:" << minorErrorCode << message; mResults.setMajorCode(minorErrorCode == Buteo::SyncResults::ABORTED ? Buteo::SyncResults::SYNC_RESULT_CANCELLED : Buteo::SyncResults::SYNC_RESULT_FAILED); mResults.setMinorCode(minorErrorCode); if (minorErrorCode == Buteo::SyncResults::AUTHENTICATION_FAILURE) { setCredentialsNeedUpdate(); } emit error(getProfileName(), message, minorErrorCode); } } void CalDavClient::authenticationError() { syncFinished(Buteo::SyncResults::AUTHENTICATION_FAILURE, QLatin1String("Authentication failed")); } Buteo::SyncProfile::SyncDirection CalDavClient::syncDirection() { FUNCTION_CALL_TRACE(lcCalDavTrace); return mSyncDirection; } Buteo::SyncProfile::ConflictResolutionPolicy CalDavClient::conflictResolutionPolicy() { FUNCTION_CALL_TRACE(lcCalDavTrace); return mConflictResPolicy; } Buteo::SyncResults CalDavClient::getSyncResults() const { FUNCTION_CALL_TRACE(lcCalDavTrace); return mResults; } void CalDavClient::getSyncDateRange(const QDateTime &sourceDate, QDateTime *fromDateTime, QDateTime *toDateTime) { if (!fromDateTime || !toDateTime) { qCWarning(lcCalDav) << "fromDate or toDate is invalid"; return; } const Buteo::Profile* client = iProfile.clientProfile(); bool valid = (client != 0); uint prevPeriod = (valid) ? client->key(SYNC_PREV_PERIOD_KEY).toUInt(&valid) : 0; *fromDateTime = sourceDate.addMonths((valid) ? -int(qMin(prevPeriod, uint(120))) : -6); uint nextPeriod = (valid) ? client->key(SYNC_NEXT_PERIOD_KEY).toUInt(&valid) : 0; *toDateTime = sourceDate.addMonths((valid) ? int(qMin(nextPeriod, uint(120))) : 12); } void CalDavClient::start() { FUNCTION_CALL_TRACE(lcCalDavTrace); if (!mAuth->username().isEmpty() && !mAuth->password().isEmpty()) { mSettings.setUsername(mAuth->username()); mSettings.setPassword(mAuth->password()); } mSettings.setAuthToken(mAuth->token()); // read the calendar user address set, to get their mailto href. PropFind *userAddressSetRequest = new PropFind(mNAManager, &mSettings, this); connect(userAddressSetRequest, &Request::finished, [this, userAddressSetRequest] { const QString userPrincipal = userAddressSetRequest->userPrincipal(); userAddressSetRequest->deleteLater(); if (!userPrincipal.isEmpty()) { // determine the mailto href for this user. mSettings.setUserPrincipal(userPrincipal); PropFind *userHrefsRequest = new PropFind(mNAManager, &mSettings, this); connect(userHrefsRequest, &Request::finished, [this, userHrefsRequest] { userHrefsRequest->deleteLater(); mSettings.setUserMailtoHref(userHrefsRequest->userMailtoHref()); listCalendars(userHrefsRequest->userHomeHref()); }); userHrefsRequest->listUserAddressSet(userPrincipal); } else { // just continue normal calendar sync. listCalendars(); } }); userAddressSetRequest->listCurrentUserPrincipal(); } void CalDavClient::listCalendars(const QString &home) { QString remoteHome(home); if (remoteHome.isEmpty()) { qCWarning(lcCalDav) << "Cannot find the calendar root for this user, guess it from account."; const struct CalendarSettings calendarSettings(mService); QList allCalendarInfo = calendarSettings.toCalendars(); if (allCalendarInfo.isEmpty()) { syncFinished(Buteo::SyncResults::INTERNAL_ERROR, QLatin1String("no calendar listed for detection")); return; } // Hacky here, try to guess the root for calendars from known // calendar paths, by removing one level. int lastIndex = allCalendarInfo[0].remotePath.lastIndexOf('/', -2); remoteHome = allCalendarInfo[0].remotePath.left(lastIndex + 1); } PropFind *calendarRequest = new PropFind(mNAManager, &mSettings, this); connect(calendarRequest, &Request::finished, this, [this, calendarRequest] { calendarRequest->deleteLater(); if (calendarRequest->errorCode() == Buteo::SyncResults::NO_ERROR // Request silently ignores this QNetworkReply::NetworkError && calendarRequest->networkError() != QNetworkReply::ContentOperationNotPermittedError) { syncCalendars(mergeAccountCalendars(calendarRequest->calendars())); } else { qCWarning(lcCalDav) << "Cannot list calendars, fallback to stored ones in account."; syncCalendars(loadAccountCalendars()); } }); calendarRequest->listCalendars(remoteHome); } void CalDavClient::syncCalendars(const QList &allCalendarInfo) { if (allCalendarInfo.isEmpty()) { syncFinished(Buteo::SyncResults::NO_ERROR, QLatin1String("No calendars for this account")); return; } mCalendar = mKCal::ExtendedCalendar::Ptr(new mKCal::ExtendedCalendar(QTimeZone::utc())); mStorage = mKCal::ExtendedCalendar::defaultStorage(mCalendar); if (!mStorage || !mStorage->open()) { syncFinished(Buteo::SyncResults::DATABASE_FAILURE, QLatin1String("unable to open calendar storage")); return; } mCalendar->setUpdateLastModifiedOnChange(false); cleanSyncRequired(); QDateTime fromDateTime; QDateTime toDateTime; getSyncDateRange(QDateTime::currentDateTime().toUTC(), &fromDateTime, &toDateTime); // for each calendar path we need to sync: // - if it is mapped to a known notebook, we need to perform quick sync // - if no known notebook exists for it, we need to create one and perform clean sync for (const PropFind::CalendarInfo &calendarInfo : allCalendarInfo) { // TODO: could use some unused field from Notebook to store "need clean sync" flag? NotebookSyncAgent *agent = new NotebookSyncAgent (mCalendar, mStorage, mNAManager, &mSettings, calendarInfo.remotePath, calendarInfo.readOnly, this); const QString &email = (calendarInfo.userPrincipal == mSettings.userPrincipal() || calendarInfo.userPrincipal.isEmpty()) ? mSettings.userMailtoHref() : QString(); if (!agent->setNotebookFromInfo(calendarInfo.displayName, calendarInfo.color, email, calendarInfo.allowEvents, calendarInfo.allowTodos, calendarInfo.allowJournals, QString::number(mService->account()->id()), getPluginName(), getProfileName())) { syncFinished(Buteo::SyncResults::DATABASE_FAILURE, QLatin1String("unable to load calendar storage")); return; } connect(agent, &NotebookSyncAgent::finished, this, &CalDavClient::notebookSyncFinished); mNotebookSyncAgents.append(agent); agent->startSync(fromDateTime, toDateTime, mSyncDirection != Buteo::SyncProfile::SYNC_DIRECTION_FROM_REMOTE, mSyncDirection != Buteo::SyncProfile::SYNC_DIRECTION_TO_REMOTE); } if (mNotebookSyncAgents.isEmpty()) { syncFinished(Buteo::SyncResults::INTERNAL_ERROR, QLatin1String("Could not add or find existing notebooks for this account")); } } void CalDavClient::clearAgents() { FUNCTION_CALL_TRACE(lcCalDavTrace); for (int i=0; ideleteLater(); } mNotebookSyncAgents.clear(); } void CalDavClient::notebookSyncFinished() { FUNCTION_CALL_TRACE(lcCalDavTrace); qCInfo(lcCalDav) << "Notebook sync finished. Total agents:" << mNotebookSyncAgents.count(); NotebookSyncAgent *agent = qobject_cast(sender()); if (!agent) { syncFinished(Buteo::SyncResults::INTERNAL_ERROR, QLatin1String("cannot get NotebookSyncAgent object")); return; } agent->disconnect(this); bool finished = true; for (int i=0; iisFinished()) { finished = false; break; } } if (finished) { bool hasFatalError = false; bool hasDatabaseErrors = false; bool hasDownloadErrors = false; bool hasUploadErrors = false; QStringList deletedNotebooks; for (int i=0; iisCompleted(); hasDownloadErrors = hasDownloadErrors || mNotebookSyncAgents[i]->hasDownloadErrors(); hasUploadErrors = hasUploadErrors || mNotebookSyncAgents[i]->hasUploadErrors(); if (!mNotebookSyncAgents[i]->applyRemoteChanges()) { qCWarning(lcCalDav) << "Unable to write notebook changes for notebook at index:" << i; hasDatabaseErrors = true; } if (mNotebookSyncAgents[i]->isDeleted()) { deletedNotebooks += mNotebookSyncAgents[i]->path(); } else { mResults.addTargetResults(mNotebookSyncAgents[i]->result()); } mNotebookSyncAgents[i]->finalize(); } removeAccountCalendars(deletedNotebooks); if (hasFatalError) { syncFinished(Buteo::SyncResults::CONNECTION_ERROR, QLatin1String("unable to complete the sync process")); } else if (hasDownloadErrors) { syncFinished(Buteo::SyncResults::ITEM_FAILURES, QLatin1String("unable to fetch all upstream changes")); } else if (hasUploadErrors) { syncFinished(Buteo::SyncResults::ITEM_FAILURES, QLatin1String("unable to upsync all local changes")); } else if (hasDatabaseErrors) { syncFinished(Buteo::SyncResults::ITEM_FAILURES, QLatin1String("unable to apply all remote changes")); } else { qCDebug(lcCalDav) << "Calendar storage saved successfully after writing notebook changes!"; syncFinished(Buteo::SyncResults::NO_ERROR); } } } void CalDavClient::setCredentialsNeedUpdate() { if (mService) { mService->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(true)); mService->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("caldav-sync"))); mService->account()->syncAndBlock(); } } buteo-sync-plugin-caldav-0.3.14/src/caldavclient.h000066400000000000000000000167071467717066200220370ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 - 2021 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef CALDAVCLIENT_H #define CALDAVCLIENT_H #include "buteo-caldav-plugin.h" #include "authhandler.h" #include "settings.h" #include "propfind.h" #include "notebooksyncagent.h" #include #include #include #include #include #include #include #include #include #include #include class QNetworkAccessManager; class Request; /* This plugin allows buteo to sync events with an online CalDAV server. Changes are read from and written to the local mkcal database. The plugin integrates with the accounts&sso libraries to sync events specific to particular user accounts. To perform a sync with this plugin, you need: - An online CalDAV server - An account created through the accounts&sso libraries - A buteo profile For example, to load the calendar at https://www.myserver.com/myusername/calendars/My_Calendar/ you would: 1) Create an accounts&sso account 2) Add a service to the account, e.g. 'my_caldav_service' The service settings must have the appropriate credentials and sign-in values, including a "CredentialsId", so that the CalDavClient can log in through the accounts sign-in framework. Also, the service needs some setting values to list the calendars to be synchronized: For example: server_address = https://www.myserver.com calendars = ["/myusername/calendars/My_Calendar/"] calendar_display_names = ["My Personal Calendar"] calendar_colors = ["#ff0000"] enabled_calendars = ["/myusername/calendars/My_Calendar/"] Multiple calendars may be listed: calendars = ["/path/to/calendarA", "/path/to/calendarB"] calendar_display_names = ["Calendar A", "Calendar B"] calendar_colors = ["#ff0000", "#0000ff"] enabled_calendars = ["/path/to/calendarB"] <-- only calendarB will be synced 3) Add a buteo profile to /.cache/msyncd/sync/. Use the supplied src/xmls/sync/caldav-sync.xml as a template. Additionally, the profile needs: - an "accountid" element with the account id - an "account_service_name" element with the account service name For example, if the ID of the account from step 1) is '55': Once you have all these, the profile can be synced via dbus using the profile name. E.g. dbus-send --session --type=method_call --print-reply \ --dest=com.meego.msyncd /synchronizer com.meego.msyncd.startSync string:caldav-sync-55 */ class BUTEOCALDAVPLUGINSHARED_EXPORT CalDavClient : public Buteo::ClientPlugin { Q_OBJECT public: CalDavClient(const QString &aPluginName, const Buteo::SyncProfile &aProfile, Buteo::PluginCbInterface *aCbInterface); virtual ~CalDavClient(); virtual bool init(); virtual bool uninit(); virtual bool startSync(); virtual void abortSync(Sync::SyncStatus aStatus = Sync::SYNC_ABORTED); virtual Buteo::SyncResults getSyncResults() const; virtual bool cleanUp(); public Q_SLOTS: virtual void connectivityStateChanged(Sync::ConnectivityType aType, bool aState); private Q_SLOTS: void start(); void authenticationError(); void notebookSyncFinished(); private: bool initConfig(); void closeConfig(); void syncFinished(Buteo::SyncResults::MinorCode minorErrorCode, const QString &message = QString()); void clearAgents(); void deleteNotebooksForAccount(int accountId, mKCal::ExtendedCalendar::Ptr calendar, mKCal::ExtendedStorage::Ptr storage); bool cleanSyncRequired(); void getSyncDateRange(const QDateTime &sourceDate, QDateTime *fromDateTime, QDateTime *toDateTime); QList loadAccountCalendars() const; QList mergeAccountCalendars(const QList &calendars) const; void removeAccountCalendars(const QStringList &paths); void listCalendars(const QString &home = QString()); void syncCalendars(const QList &allCalendarInfo); Buteo::SyncProfile::SyncDirection syncDirection(); Buteo::SyncProfile::ConflictResolutionPolicy conflictResolutionPolicy(); void setCredentialsNeedUpdate(); mutable QScopedPointer mProcessMutex; QList mNotebookSyncAgents; QNetworkAccessManager* mNAManager; Accounts::Manager* mManager; QSharedPointer mService; AuthHandler* mAuth; mKCal::ExtendedCalendar::Ptr mCalendar; mKCal::ExtendedStorage::Ptr mStorage; Buteo::SyncResults mResults; Sync::SyncStatus mSyncStatus; Buteo::SyncProfile::SyncDirection mSyncDirection; Buteo::SyncProfile::ConflictResolutionPolicy mConflictResPolicy; Settings mSettings; friend class tst_CalDavClient; }; class CalDavClientLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.CalDavClientLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: /*! \brief Creates CalDav client plugin * * @param aPluginName Name of this client plugin * @param aProfile Profile to use * @param aCbInterface Pointer to the callback interface * @return Client plugin on success, otherwise NULL */ Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // CALDAVCLIENT_H buteo-sync-plugin-caldav-0.3.14/src/delete.cpp000066400000000000000000000042141467717066200211710ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "delete.h" #include "settings.h" #include #include #include "logging.h" #define PROP_INCIDENCE_URI "uri" static const QString VCalExtension = QStringLiteral(".ics"); Delete::Delete(QNetworkAccessManager *manager, Settings *settings, QObject *parent) : Request(manager, settings, "DELETE", parent) { FUNCTION_CALL_TRACE(lcCalDavTrace); } void Delete::deleteEvent(const QString &href) { FUNCTION_CALL_TRACE(lcCalDavTrace); QNetworkRequest request; prepareRequest(&request, href); QNetworkReply *reply = mNAManager->sendCustomRequest(request, REQUEST_TYPE.toLatin1()); reply->setProperty(PROP_INCIDENCE_URI, href); debugRequest(request, QStringLiteral("")); connect(reply, SIGNAL(finished()), this, SLOT(requestFinished())); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(slotSslErrors(QList))); } void Delete::handleReply(QNetworkReply *reply) { FUNCTION_CALL_TRACE(lcCalDavTrace); const QString &uri = reply->property(PROP_INCIDENCE_URI).toString(); if (reply->error() == QNetworkReply::ContentNotFoundError) { // Consider a success if the content does not exist on server. finishedWithSuccess(uri); } else { finishedWithReplyResult(uri, reply); } } buteo-sync-plugin-caldav-0.3.14/src/delete.h000066400000000000000000000024411467717066200206360ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef DELETE_H #define DELETE_H #include "request.h" #include #include #include class QNetworkAccessManager; class Settings; class Delete : public Request { Q_OBJECT public: explicit Delete(QNetworkAccessManager *manager, Settings *settings, QObject *parent = 0); void deleteEvent(const QString &href); protected: virtual void handleReply(QNetworkReply *reply); }; #endif // DELETE_H buteo-sync-plugin-caldav-0.3.14/src/incidencehandler.cpp000066400000000000000000000145501467717066200232120ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Bea Lam * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "incidencehandler.h" #include #include "logging.h" #include #include #define PROP_DTEND_ADDED_USING_DTSTART "dtend-added-as-dtstart" IncidenceHandler::IncidenceHandler() { } IncidenceHandler::~IncidenceHandler() { } // A given incidence has been added or modified locally. // To upsync the change, we need to construct the .ics data to upload to server. // Since the incidence may be an occurrence or recurring series incidence, // we cannot simply convert the incidence to iCal data, but instead we have to // upsync an .ics containing the whole recurring series. QString IncidenceHandler::toIcs(const KCalendarCore::Incidence::Ptr incidence, const KCalendarCore::Incidence::List instances) { // create an in-memory calendar // add to it the required incidences (ie, check if has recurrenceId -> load parent and all instances; etc) // for each of those, we need to do the IncidenceToExport() modifications first // then, export from that calendar to .ics file. KCalendarCore::MemoryCalendar::Ptr memoryCalendar(new KCalendarCore::MemoryCalendar(QTimeZone::utc())); KCalendarCore::Incidence::Ptr exportableIncidence = IncidenceHandler::incidenceToExport(incidence, instances); // store the base recurring event into the in-memory calendar if (!memoryCalendar->addIncidence(exportableIncidence)) { qCWarning(lcCalDav) << "Unable to add base series event to in-memory calendar for incidence:" << incidence->uid() << ":" << incidence->recurrenceId().toString(); return QString(); } // now create the persistent occurrences in the in-memory calendar for (KCalendarCore::Incidence::Ptr instance : instances) { KCalendarCore::Incidence::Ptr exportableOccurrence = IncidenceHandler::incidenceToExport(instance); if (!memoryCalendar->addIncidence(exportableOccurrence)) { qCWarning(lcCalDav) << "Unable to add this incidence to in-memory calendar for export:" << instance->uid() << instance->recurrenceId().toString(); return QString(); } } KCalendarCore::ICalFormat icalFormat; return icalFormat.toString(memoryCalendar, QString(), false); } KCalendarCore::Incidence::Ptr IncidenceHandler::incidenceToExport(KCalendarCore::Incidence::Ptr sourceIncidence, const KCalendarCore::Incidence::List &instances) { KCalendarCore::Incidence::Ptr incidence = QSharedPointer(sourceIncidence->clone()); // check to see if the UID is of the special form: NBUID:NotebookUid:EventUid. If so, trim it. if (incidence->uid().startsWith(QStringLiteral("NBUID:"))) { QString oldUid = incidence->uid(); QString trimmedUid = oldUid.mid(oldUid.indexOf(':', 6)+1); // remove NBUID:NotebookUid: incidence->setUid(trimmedUid); // leaving just the EventUid. } // remove any (obsolete) markers that tell us that the time was added by us incidence->removeCustomProperty("buteo", "dtstart-date_only"); incidence->removeCustomProperty("buteo", "dtend-date_only"); // remove any URI or ETAG data we insert into the event for sync purposes. incidence->removeCustomProperty("buteo", "uri"); incidence->removeCustomProperty("buteo", "etag"); const QStringList &comments(incidence->comments()); for (const QString &comment : comments) { if ((comment.startsWith("buteo:caldav:uri:") || comment.startsWith("buteo:caldav:detached-and-synced") || comment.startsWith("buteo:caldav:etag:")) && incidence->removeComment(comment)) { qCDebug(lcCalDav) << "Discarding buteo-prefixed comment:" << comment; } } // remove EXDATE values from the recurring incidence which correspond to the persistent occurrences (instances) if (incidence->recurs()) { for (KCalendarCore::Incidence::Ptr instance : instances) { KCalendarCore::DateTimeList exDateTimes = incidence->recurrence()->exDateTimes(); exDateTimes.removeAll(instance->recurrenceId()); incidence->recurrence()->setExDateTimes(exDateTimes); qCDebug(lcCalDav) << "Discarding exdate:" << instance->recurrenceId().toString(); } } switch (incidence->type()) { case KCalendarCore::IncidenceBase::TypeEvent: { KCalendarCore::Event::Ptr event = incidence.staticCast(); bool eventIsAllDay = event->allDay(); if (eventIsAllDay) { bool sendWithoutDtEnd = !event->customProperty("buteo", PROP_DTEND_ADDED_USING_DTSTART).isEmpty() && (event->dtStart() == event->dtEnd()); event->removeCustomProperty("buteo", PROP_DTEND_ADDED_USING_DTSTART); if (sendWithoutDtEnd) { // A single-day all-day event was received without a DTEND, and it is still a single-day // all-day event, so remove the DTEND before upsyncing. qCDebug(lcCalDav) << "Removing DTEND from" << incidence->uid(); event->setDtEnd(QDateTime()); } } // setting dtStart/End changes the allDay value, so ensure it is still set to true if needed. if (eventIsAllDay) { event->setAllDay(true); } break; } case KCalendarCore::IncidenceBase::TypeTodo: break; default: qCDebug(lcCalDav) << "Incidence type not supported; cannot create proper exportable version"; break; } return incidence; } buteo-sync-plugin-caldav-0.3.14/src/incidencehandler.h000066400000000000000000000032001467717066200226450ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Bea Lam * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef INCIDENCEHANDLER_P_H #define INCIDENCEHANDLER_P_H #include #include #include #include #include class IncidenceHandler { public: static QString toIcs(const KCalendarCore::Incidence::Ptr incidence, const KCalendarCore::Incidence::List instances = KCalendarCore::Incidence::List()); private: IncidenceHandler(); ~IncidenceHandler(); static KCalendarCore::Incidence::Ptr incidenceToExport(KCalendarCore::Incidence::Ptr sourceIncidence, const KCalendarCore::Incidence::List &instances = KCalendarCore::Incidence::List()); friend class tst_NotebookSyncAgent; friend class tst_IncidenceHandler; }; #endif // INCIDENCEHANDLER_P_H buteo-sync-plugin-caldav-0.3.14/src/logging.cpp000066400000000000000000000020041467717066200213500ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2021 Jolla Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "logging.h" Q_LOGGING_CATEGORY(lcCalDav, "buteo.plugin.caldav", QtWarningMsg) Q_LOGGING_CATEGORY(lcCalDavProtocol, "buteo.plugin.caldav.protocol", QtWarningMsg) Q_LOGGING_CATEGORY(lcCalDavTrace, "buteo.plugin.caldav.trace", QtWarningMsg) buteo-sync-plugin-caldav-0.3.14/src/logging.h000066400000000000000000000020041467717066200210150ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2021 Jolla Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef CALDAV_LOGGING_H #define CALDAV_LOGGING_H #include #include Q_DECLARE_LOGGING_CATEGORY(lcCalDav) Q_DECLARE_LOGGING_CATEGORY(lcCalDavProtocol) Q_DECLARE_LOGGING_CATEGORY(lcCalDavTrace) #endif buteo-sync-plugin-caldav-0.3.14/src/main.cpp000066400000000000000000000063741467717066200206640ustar00rootroot00000000000000#include #include #include #include "report.h" #include "put.h" #include "delete.h" int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); Report *report = new Report; report->getAllEvents("https://ec2-175-41-181-116.ap-southeast-1.compute.amazonaws.com/davical/caldav.php/mani/calendar/", "ya29.AHES6ZSTIDrnhbog3SRIbWBzp8IjcAy9dqk7CnIWzfgUgeEMepk-qA"); // qDebug() << "-----------------------------------Starting MultiGET request for 4 items -------------------------------"; // QStringList idList; // idList << "/caldav/v2/mobilitas123%40gmail.com/events/5pc866q9pd02gfqhkka38b1938%40google.com.ics"; // idList << "/caldav/v2/mobilitas123%40gmail.com/events/g785upk1g9gev0a3pljdr3a0fs%40google.com.ics"; // idList << "/caldav/v2/mobilitas123%40gmail.com/events/rvft8rh86c3od7hd1ai52sls88%40google.com.ics"; // idList << "/caldav/v2/mobilitas123%40gmail.com/events/sk91ip6bkc4i744or97t96l70o%40google.com.ics"; // idList << "/caldav/v2/mobilitas123%40gmail.com/events/sac4samagmkol4a2j2lirol1g8%40google.com.ics"; // report->multiGetEvents("https://apidata.googleusercontent.com/caldav/v2/mobilitas123@gmail.com/events/", // "ya29.AHES6ZSuJai_DVqjdu4tDYkvzKlNGrFPaFLzmLEoHuIneSsYlRV5YQ", idList); // QString data = "BEGIN:VCALENDAR\nPRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0\nCALSCALE:GREGORIAN\nX-WR-CALNAME:mobilitas123@gmail.com\nX-WR-TIMEZONE:Europe/London\BEGIN:VTIMEZONE\nTZID:Europe/London\nX-LIC-LOCATION:Europe/London\n" \ // "BEGIN:DAYLIGHT\nTZOFFSETFROM:+0000\nTZOFFSETTO:+0100\nTZNAME:BST\nDTSTART:19700329T010000\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\nEND:DAYLIGHT\nBEGIN:STANDARD\nTZOFFSETFROM:+0100\nTZOFFSETTO:+0000\nTZNAME:GMT\n" \ // "DTSTART:19701025T020000\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nDTSTART;TZID=Europe/London:20121121T080000\nDTEND;TZID=Europe/London:20121121T090000\n" \ // "DTSTAMP:20121120T071549Z\nUID:sac4samagmkol4a2j2lirol1g8@google.com\nCREATED:20121120T071549Z\nDESCRIPTION:sdfsdfsdfsdfsfsfsFbfbfbbf\nLAST-MODIFIED:20131002T071549Z\nLOCATION:Hrgrbhjjhjhkjkjhjkjkjkjkjkjkkkjkjljlrb\nSEQUENCE:0\nSTATUS:CONFIRMED\nSUMMARY:manish\nTRANSP:OPAQUE\nCATEGORIES:http://schemas.google.com/g/2005#event\nEND:VEVENT\nEND:VCALENDAR"; // Put *put = new Put; // put->createEvent("https://apidata.googleusercontent.com/caldav/v2/mobilitas123%40gmail.com/events/", // "ya29.AHES6ZSuJai_DVqjdu4tDYkvzKlNGrFPaFLzmLEoHuIneSsYlRV5YQ", ""); // Delete *del = new Delete; // del->deleteEvent("https://apidata.googleusercontent.com/caldav/v2/mobilitas123%40gmail.com/events/g785upk1g9gev0a3pljdr3a0fs%40google.com.ics", // "ya29.AHES6ZRv3tXPU-pSew0UCxyLbYaGtyt6oUIzMtXBAsDU5wPVSDGmgw"); // QStringList idList; // idList << "/caldav/v2/mobilitas123%40gmail.com/events/g785upk1g9gev0a3pljdr3a0fs%40google.com.ics"; // Report *rep = new Report; // rep->multiGetEvents("https://apidata.googleusercontent.com/caldav/v2/mobilitas123@gmail.com/events", // "ya29.AHES6ZRv3tXPU-pSew0UCxyLbYaGtyt6oUIzMtXBAsDU5wPVSDGmgw", idList); return a.exec(); } buteo-sync-plugin-caldav-0.3.14/src/notebooksyncagent.cpp000066400000000000000000001731321467717066200234710ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Bea Lam * Stephan Rave * Chris Adams * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "notebooksyncagent.h" #include "incidencehandler.h" #include "settings.h" #include "report.h" #include "put.h" #include "delete.h" #include "reader.h" #include "logging.h" #include #include #include #define NOTEBOOK_FUNCTION_CALL_TRACE qCDebug(lcCalDavTrace) << Q_FUNC_INFO << (mNotebook ? mNotebook->account() : "") namespace { // mKCal deleted custom properties of deleted incidences. // This was problematic for sync, as we need some fields // (resource URI and ETAG) in order to sync properly. // Hence, we abuse the COMMENTS field of the incidence. QString storedIncidenceHrefUri(KCalendarCore::Incidence::Ptr incidence) { const QStringList &comments(incidence->comments()); for (const QString &comment : comments) { if (comment.startsWith("buteo:caldav:uri:")) { QString uri = comment.mid(17); if (uri.contains('%')) { // if it contained a % or a space character, we percent-encoded // the uri before storing it, because otherwise kcal doesn't // split the comments properly. uri = QUrl::fromPercentEncoding(uri.toUtf8()); qCDebug(lcCalDav) << "URI comment was percent encoded:" << comment << ", returning uri:" << uri; } if (uri.isEmpty()) { qCWarning(lcCalDav) << "Stored uri was empty for:" << incidence->uid() << incidence->recurrenceId().toString(); } return uri; } } qCWarning(lcCalDav) << "Returning empty uri for:" << incidence->uid() << incidence->recurrenceId().toString(); return QString(); } QString createIncidenceHrefUri(KCalendarCore::Incidence::Ptr incidence, const QString &remoteCalendarPath) { if (incidence->uid().startsWith(QString::fromLatin1("NBUID:"))) { const QString uid = incidence->uid(); return remoteCalendarPath + uid.mid(uid.indexOf(':', 6)+1) + ".ics"; // remove NBUID:NotebookUid: } else { // In case UID is coming from an importation it can be // whatever. A proper sanitation woud be to use QUrl::toPercentEncoding() // but the percent encoded '/' character is creating issue with // some server in multiget requests. return remoteCalendarPath + incidence->uid().replace('/', '-') + ".ics"; } } void setIncidenceHrefUri(KCalendarCore::Incidence::Ptr incidence, const QString &hrefUri) { const QStringList &comments(incidence->comments()); for (const QString &comment : comments) { if (comment.startsWith("buteo:caldav:uri:") && incidence->removeComment(comment)) { break; } } if (hrefUri.contains('%') || hrefUri.contains(' ')) { // need to percent-encode the uri before storing it, // otherwise mkcal doesn't split the comments correctly. incidence->addComment(QStringLiteral("buteo:caldav:uri:%1").arg(QString::fromUtf8(QUrl::toPercentEncoding(hrefUri)))); } else { incidence->addComment(QStringLiteral("buteo:caldav:uri:%1").arg(hrefUri)); } } QString incidenceETag(KCalendarCore::Incidence::Ptr incidence) { const QStringList &comments(incidence->comments()); for (const QString &comment : comments) { if (comment.startsWith("buteo:caldav:etag:")) { return comment.mid(18); } } return QString(); } void setIncidenceETag(KCalendarCore::Incidence::Ptr incidence, const QString &etag) { const QStringList &comments(incidence->comments()); for (const QString &comment : comments) { if (comment.startsWith("buteo:caldav:etag:") && incidence->removeComment(comment)) { break; } } incidence->addComment(QStringLiteral("buteo:caldav:etag:%1").arg(etag)); } void updateIncidenceHrefEtag(KCalendarCore::Incidence::Ptr incidence, const QString &href, const QString &etag) { // Set the URI and the ETAG property to the required values. qCDebug(lcCalDav) << "Adding URI and ETAG to incidence:" << incidence->uid() << incidence->recurrenceId().toString() << ":" << href << etag; if (!href.isEmpty()) setIncidenceHrefUri(incidence, href); if (!etag.isEmpty()) setIncidenceETag(incidence, etag); if (incidence->recurrenceId().isValid()) { // Add a flag to distinguish persistent exceptions that have // been detached during the sync process (with the flag) // or by a call to dissociateSingleOccurrence() outside // of the sync process (in that later case, the incidence // will have to be treated as a local addition of a persistent // exception, see the calculateDelta() function). incidence->removeComment("buteo:caldav:detached-and-synced"); incidence->addComment("buteo:caldav:detached-and-synced"); } } bool isCopiedDetachedIncidence(KCalendarCore::Incidence::Ptr incidence) { if (incidence->recurrenceId().isNull()) return false; const QStringList &comments(incidence->comments()); for (const QString &comment : comments) { if (comment == "buteo:caldav:detached-and-synced") { return false; } } return true; } bool incidenceWithin(KCalendarCore::Incidence::Ptr incidence, const QDateTime &from, const QDateTime &to) { return incidence->dtStart() <= to && (!incidence->recurs() || !incidence->recurrence()->endDateTime().isValid() || incidence->recurrence()->endDateTime() >= from) && (incidence->recurs() || incidence->dateTime(KCalendarCore::Incidence::RoleDisplayEnd) >= from); } typedef enum { REMOTE, LOCAL } Target; void summarizeResults(Buteo::TargetResults *results, Target target, Buteo::TargetResults::ItemOperation operation, const QHash &failingHrefs, const KCalendarCore::Incidence::List &incidences, const QString &remotePath = QString()) { for (int i = 0; i < incidences.size(); i++) { const QString href = storedIncidenceHrefUri(incidences[i]); const QString uid(incidences[i]->instanceIdentifier()); const QHash::ConstIterator failure = failingHrefs.find(href.isEmpty() ? createIncidenceHrefUri(incidences[i], remotePath) : href); const Buteo::TargetResults::ItemOperationStatus status = failure != failingHrefs.constEnd() ? Buteo::TargetResults::ITEM_OPERATION_FAILED : Buteo::TargetResults::ITEM_OPERATION_SUCCEEDED; const QString data = failure != failingHrefs.constEnd() ? QString::fromUtf8(failure.value()) : QString(); if (target == LOCAL) { results->addLocalDetails(uid, operation, status, data); } else { results->addRemoteDetails(uid, operation, status, data); } } } const QByteArray APP = QByteArrayLiteral("VOLATILE"); const QByteArray NAME = QByteArrayLiteral("SYNC-FAILURE"); const QByteArray RESOLUTION = QByteArrayLiteral("SYNC-FAILURE-RESOLUTION"); void flagUploadFailure(const QHash &failingHrefs, const KCalendarCore::Incidence::List &incidences, const QString &remotePath = QString()) { for (int i = 0; i < incidences.size(); i++) { const QString href = storedIncidenceHrefUri(incidences[i]); if (href.isEmpty() && failingHrefs.contains(createIncidenceHrefUri(incidences[i], remotePath))) { incidences[i]->setCustomProperty(APP, NAME, QStringLiteral("upload-new")); } else if (!href.isEmpty() && failingHrefs.contains(href)) { incidences[i]->setCustomProperty(APP, NAME, QStringLiteral("upload")); } else { incidences[i]->removeCustomProperty(APP, NAME); incidences[i]->removeCustomProperty(APP, RESOLUTION); } } } void flagUpdateSuccess(const KCalendarCore::Incidence::Ptr &incidence) { incidence->removeCustomProperty(APP, NAME); incidence->removeCustomProperty(APP, RESOLUTION); } void flagUpdateFailure(const KCalendarCore::Incidence::Ptr &incidence) { incidence->setCustomProperty(APP, NAME, QStringLiteral("update")); } void flagDeleteFailure(const KCalendarCore::Incidence::Ptr &incidence) { incidence->setCustomProperty(APP, NAME, QStringLiteral("delete")); } bool isFlagged(const KCalendarCore::Incidence::Ptr &incidence) { return !incidence->customProperty(APP, NAME).isEmpty(); } bool retryUploadFailure(const KCalendarCore::Incidence::Ptr &incidence) { return incidence->customProperty(APP, NAME).startsWith(QStringLiteral("upload")) && incidence->customProperty(APP, RESOLUTION).isEmpty(); } bool retryUpdateFailure(const KCalendarCore::Incidence::Ptr &incidence) { return incidence->customProperty(APP, NAME) == QStringLiteral("update") && incidence->customProperty(APP, RESOLUTION).isEmpty(); } bool retryDeleteFailure(const KCalendarCore::Incidence::Ptr &incidence) { return incidence->customProperty(APP, NAME) == QStringLiteral("delete") && incidence->customProperty(APP, RESOLUTION).isEmpty(); } bool resetUploadFailure(const KCalendarCore::Incidence::Ptr &incidence) { return incidence->customProperty(APP, NAME) == QStringLiteral("upload") && incidence->customProperty(APP, RESOLUTION) == QStringLiteral("server-reset"); } bool resetUpdateFailure(const KCalendarCore::Incidence::Ptr &incidence) { return incidence->customProperty(APP, NAME) == QStringLiteral("update") && incidence->customProperty(APP, RESOLUTION) == QStringLiteral("device-reset"); } bool resetDeleteFailure(const KCalendarCore::Incidence::Ptr &incidence) { return incidence->customProperty(APP, NAME) == QStringLiteral("delete") && incidence->customProperty(APP, RESOLUTION) == QStringLiteral("device-reset"); } } NotebookSyncAgent::NotebookSyncAgent(mKCal::ExtendedCalendar::Ptr calendar, mKCal::ExtendedStorage::Ptr storage, QNetworkAccessManager *networkAccessManager, Settings *settings, const QString &encodedRemotePath, bool readOnlyFlag, QObject *parent) : QObject(parent) , mNetworkManager(networkAccessManager) , mSettings(settings) , mCalendar(calendar) , mStorage(storage) , mNotebook(0) , mEncodedRemotePath(encodedRemotePath) , mSyncMode(NoSyncMode) , mRetriedReport(false) , mNotebookNeedsDeletion(false) , mEnableUpsync(true) , mEnableDownsync(true) , mReadOnlyFlag(readOnlyFlag) { // the calendar path may be percent-encoded. Return UTF-8 QString. mRemoteCalendarPath = QUrl::fromPercentEncoding(mEncodedRemotePath.toUtf8()); // Yahoo! seems to double-percent-encode for some reason if (mSettings->serverAddress().contains(QStringLiteral("caldav.calendar.yahoo.com"))) { mRemoteCalendarPath = QUrl::fromPercentEncoding(mRemoteCalendarPath.toUtf8()); } } NotebookSyncAgent::~NotebookSyncAgent() { NOTEBOOK_FUNCTION_CALL_TRACE; if (!mRequests.isEmpty()) { abort(); } } void NotebookSyncAgent::abort() { NOTEBOOK_FUNCTION_CALL_TRACE; QList requests = mRequests.toList(); for (int i=0; ideleteLater(); } mRequests.clear(); emit finished(); } static const QByteArray PATH_PROPERTY = QByteArrayLiteral("remoteCalendarPath"); static const QByteArray EMAIL_PROPERTY = QByteArrayLiteral("userPrincipalEmail"); static const QByteArray SERVER_COLOR_PROPERTY = QByteArrayLiteral("serverColor"); bool NotebookSyncAgent::setNotebookFromInfo(const QString ¬ebookName, const QString &color, const QString &userEmail, bool allowEvents, bool allowTodos, bool allowJournals, const QString &accountId, const QString &pluginName, const QString &syncProfile) { mNotebook = static_cast(0); // Look for an already existing notebook in storage for this account and path. const mKCal::Notebook::List notebooks = mStorage->notebooks(); for (mKCal::Notebook::Ptr notebook : notebooks) { if (notebook->account() == accountId && (notebook->customProperty(PATH_PROPERTY) == mRemoteCalendarPath || notebook->syncProfile().endsWith(QStringLiteral(":%1").arg(mRemoteCalendarPath)))) { qCDebug(lcCalDav) << "found notebook:" << notebook->uid() << "for remote calendar:" << mRemoteCalendarPath; mNotebook = notebook; if (!color.isEmpty() && notebook->customProperty(SERVER_COLOR_PROPERTY) != color) { if (!notebook->customProperty(SERVER_COLOR_PROPERTY).isEmpty()) { // Override user-selected notebook color only on each server change // and not if there was no server color saved. mNotebook->setColor(color); } mNotebook->setCustomProperty(SERVER_COLOR_PROPERTY, color); } mNotebook->setName(notebookName); mNotebook->setSyncProfile(syncProfile); mNotebook->setCustomProperty(EMAIL_PROPERTY, userEmail); mNotebook->setPluginName(pluginName); mNotebook->setEventsAllowed(allowEvents); mNotebook->setTodosAllowed(allowTodos); mNotebook->setJournalsAllowed(allowJournals); return true; } } qCDebug(lcCalDav) << "no notebook exists for" << mRemoteCalendarPath; // or create a new one mNotebook = mKCal::Notebook::Ptr(new mKCal::Notebook(notebookName, QString())); mNotebook->setAccount(accountId); mNotebook->setPluginName(pluginName); mNotebook->setSyncProfile(syncProfile); mNotebook->setCustomProperty(PATH_PROPERTY, mRemoteCalendarPath); mNotebook->setCustomProperty(EMAIL_PROPERTY, userEmail); if (!color.isEmpty()) { mNotebook->setColor(color); mNotebook->setCustomProperty(SERVER_COLOR_PROPERTY, color); } mNotebook->setEventsAllowed(allowEvents); mNotebook->setTodosAllowed(allowTodos); mNotebook->setJournalsAllowed(allowJournals); return true; } void NotebookSyncAgent::startSync(const QDateTime &fromDateTime, const QDateTime &toDateTime, bool withUpsync, bool withDownsync) { NOTEBOOK_FUNCTION_CALL_TRACE; if (!mNotebook) { qCDebug(lcCalDav) << "no notebook to sync."; return; } // Store sync time before sync is completed to avoid loosing events // that may be inserted server side between now and the termination // of the process. mNotebookSyncedDateTime = QDateTime::currentDateTimeUtc(); mFromDateTime = fromDateTime; mToDateTime = toDateTime; mEnableUpsync = withUpsync; mEnableDownsync = withDownsync; if (mNotebook->syncDate().isNull()) { /* Slow sync mode: 1) Get all calendars on the server using Report::getAllEvents() 2) Save all received calendar data to disk. Step 2) is triggered by CalDavClient once *all* notebook syncs have finished. */ qCDebug(lcCalDav) << "Start slow sync for notebook:" << mNotebook->name() << "for account" << mNotebook->account() << "between" << fromDateTime << "to" << toDateTime; mSyncMode = SlowSync; // Even if down sync is disabled in profile, we down sync the // remote calendar the first time, by design. sendReportRequest(); } else { /* Quick sync mode: 1) Get all remote calendar etags and updated calendar data from the server using Report::getAllETags() 2) Get all local changes since the last sync 3) Filter out local changes that were actually remote changes written by step 5) of this sequence from a previous sync 4) Send the local changes to the server using Put and Delete requests 5) Write the remote calendar changes to disk. Step 5) is triggered by CalDavClient once *all* notebook syncs have finished. */ qCDebug(lcCalDav) << "Start quick sync for notebook:" << mNotebook->uid() << "between" << fromDateTime << "to" << toDateTime << ", sync changes since" << mNotebook->syncDate(); mSyncMode = QuickSync; fetchRemoteChanges(); } } void NotebookSyncAgent::sendReportRequest(const QStringList &remoteUris) { // must be m_syncMode = SlowSync. Report *report = new Report(mNetworkManager, mSettings); mRequests.insert(report); connect(report, &Report::finished, this, &NotebookSyncAgent::reportRequestFinished); if (remoteUris.isEmpty()) { report->getAllEvents(mRemoteCalendarPath, mFromDateTime, mToDateTime); } else { report->multiGetEvents(mRemoteCalendarPath, remoteUris); } } void NotebookSyncAgent::fetchRemoteChanges() { NOTEBOOK_FUNCTION_CALL_TRACE; // must be m_syncMode = QuickSync. Report *report = new Report(mNetworkManager, mSettings); mRequests.insert(report); connect(report, &Report::finished, this, &NotebookSyncAgent::processETags); report->getAllETags(mRemoteCalendarPath, mFromDateTime, mToDateTime); } void NotebookSyncAgent::reportRequestFinished(const QString &uri) { NOTEBOOK_FUNCTION_CALL_TRACE; Report *report = qobject_cast(sender()); if (!report) { setFatal(uri, "Internal reportRequest error"); return; } qCDebug(lcCalDav) << "report request finished with result:" << report->errorCode() << report->errorMessage(); if (report->errorCode() == Buteo::SyncResults::NO_ERROR) { // NOTE: we don't store the remote artifacts yet // Instead, we just emit finished (for this notebook) // Once ALL notebooks are finished, then we apply the remote changes. // This prevents the worst partial-sync issues. mReceivedCalendarResources += report->receivedCalendarResources(); qCDebug(lcCalDav) << "Report request finished: received:" << report->receivedCalendarResources().length() << "iCal blobs"; } else if (mSyncMode == SlowSync && report->networkError() == QNetworkReply::AuthenticationRequiredError && !mRetriedReport) { // Yahoo sometimes fails the initial request with an authentication error. Let's try once more qCWarning(lcCalDav) << "Retrying REPORT after request failed with QNetworkReply::AuthenticationRequiredError"; mRetriedReport = true; sendReportRequest(); } else if (mSyncMode == SlowSync && report->networkError() == QNetworkReply::ContentNotFoundError) { // The remote calendar resource was removed after we created the account but before first sync. // We don't perform resource discovery in CalDAV during each sync cycle, // so we can have local calendar metadata for remotely removed calendars. // In this case, we just skip sync of this calendar, as it was deleted. mNotebookNeedsDeletion = true; qCDebug(lcCalDav) << "calendar" << uri << "was deleted remotely, skipping sync locally."; } else if (mSyncMode == SlowSync) { setFatal(uri, report->errorData()); return; } else { for (const QString &href : report->fetchedUris()) { mFailingUpdates.insert(href, report->errorData()); } } requestFinished(report); } void NotebookSyncAgent::processETags(const QString &uri) { NOTEBOOK_FUNCTION_CALL_TRACE; Report *report = qobject_cast(sender()); if (!report) { setFatal(uri, "Internal processETags error"); return; } qCDebug(lcCalDav) << "fetch etags finished with result:" << report->errorCode() << report->errorMessage(); if (report->errorCode() == Buteo::SyncResults::NO_ERROR) { qCDebug(lcCalDav) << "Process tags for server path" << uri; // we have a hash from resource href-uri to resource info (including etags). QHash remoteHrefUriToEtags; for (const Reader::CalendarResource &resource : report->receivedCalendarResources()) { if (!resource.href.contains(mRemoteCalendarPath)) { qCWarning(lcCalDav) << "href does not contain server path:" << resource.href << ":" << mRemoteCalendarPath; setFatal(uri, "Mismatch in hrefs from server response."); return; } remoteHrefUriToEtags.insert(resource.href, resource.etag); } // calculate the local and remote delta. if (!calculateDelta(remoteHrefUriToEtags, &mLocalAdditions, &mLocalModifications, &mLocalDeletions, &mRemoteChanges, &mRemoteDeletions)) { qCWarning(lcCalDav) << "unable to calculate the sync delta for:" << mRemoteCalendarPath; setFatal(uri, "Unable to calculate the sync delta."); return; } if (mEnableDownsync && !mRemoteChanges.isEmpty()) { // some incidences have changed on the server, so fetch the new details sendReportRequest(mRemoteChanges.toList()); } sendLocalChanges(); } else if (report->networkError() == QNetworkReply::AuthenticationRequiredError && !mRetriedReport) { // Yahoo sometimes fails the initial request with an authentication error. Let's try once more qCWarning(lcCalDav) << "Retrying ETAG REPORT after request failed with QNetworkReply::AuthenticationRequiredError"; mRetriedReport = true; fetchRemoteChanges(); } else if (report->networkError() == QNetworkReply::ContentNotFoundError) { // The remote calendar resource was removed. // We don't perform resource discovery in CalDAV during each sync cycle, // so we can have local calendars which mirror remotely-removed calendars. // In this situation, we need to delete the local calendar. mNotebookNeedsDeletion = true; qCDebug(lcCalDav) << "calendar" << uri << "was deleted remotely, marking for deletion locally:" << mNotebook->name(); } else { setFatal(uri, report->errorData()); return; } requestFinished(report); } void NotebookSyncAgent::sendLocalChanges() { NOTEBOOK_FUNCTION_CALL_TRACE; mFailingUploads.clear(); if (!mLocalAdditions.count() && !mLocalModifications.count() && !mLocalDeletions.count()) { // no local changes to upsync. // we're finished syncing. qCDebug(lcCalDav) << "no local changes to upsync - finished with notebook" << mNotebook->name() << mRemoteCalendarPath; return; } else if (!mEnableUpsync) { qCDebug(lcCalDav) << "Not upsyncing local changes, upsync disable in profile."; return; } else if (mReadOnlyFlag) { qCDebug(lcCalDav) << "Not upsyncing local changes, upstream read only calendar."; return; } else { qCDebug(lcCalDav) << "upsyncing local changes: A/M/R:" << mLocalAdditions.count() << "/" << mLocalModifications.count() << "/" << mLocalDeletions.count(); } // For deletions, if a persistent exception is deleted we may need to do a PUT // containing all of the still-existing events in the series. // Hence, we first need to find out if any deletion is a lone-persistent-exception deletion. QMultiHash uidToRecurrenceIdDeletions; QHash uidToUri; // we cannot look up custom properties of deleted incidences, so cache them here. for (KCalendarCore::Incidence::Ptr localDeletion : const_cast(mLocalDeletions)) { uidToRecurrenceIdDeletions.insert(localDeletion->uid(), localDeletion->recurrenceId()); uidToUri.insert(localDeletion->uid(), storedIncidenceHrefUri(localDeletion)); } // now send DELETEs as required, and PUTs as required. const QStringList keys = uidToRecurrenceIdDeletions.uniqueKeys(); for (const QString &uid : keys) { QList recurrenceIds = uidToRecurrenceIdDeletions.values(uid); if (!recurrenceIds.contains(QDateTime())) { mStorage->load(uid); KCalendarCore::Incidence::Ptr recurringSeries = mCalendar->incidence(uid); if (recurringSeries) { mLocalModifications.append(recurringSeries); continue; // finished with this deletion. } else { qCWarning(lcCalDav) << "Unable to load recurring incidence for deleted exception; deleting entire series instead"; // fall through to the DELETE code below. } } // the whole series is being deleted; can DELETE. QString remoteUri = uidToUri.value(uid); qCDebug(lcCalDav) << "deleting whole series:" << remoteUri << "with uid:" << uid; Delete *del = new Delete(mNetworkManager, mSettings); mRequests.insert(del); connect(del, &Delete::finished, this, &NotebookSyncAgent::nonReportRequestFinished); del->deleteEvent(remoteUri); } // Incidence will be actually purged only if all operations succeed. mPurgeList += mLocalDeletions; mSentUids.clear(); KCalendarCore::Incidence::List toUpload(mLocalAdditions + mLocalModifications); for (int i = 0; i < toUpload.count(); i++) { QString href = storedIncidenceHrefUri(toUpload[i]); if (href.isEmpty()) href = createIncidenceHrefUri(toUpload[i], mRemoteCalendarPath); if (mSentUids.contains(href)) { qCDebug(lcCalDav) << "Already handled upload" << i << "via series update"; continue; // already handled this one, as a result of a previous update of another occurrence in the series. } QString etag = incidenceETag(toUpload[i]); QString icsData; if (toUpload[i]->recurs() || toUpload[i]->hasRecurrenceId()) { if (mStorage->load(toUpload[i]->uid())) { KCalendarCore::Incidence::Ptr recurringIncidence(toUpload[i]->recurs() ? toUpload[i] : mCalendar->incidence(toUpload[i]->uid())); if (recurringIncidence) { etag = incidenceETag(recurringIncidence); icsData = IncidenceHandler::toIcs(recurringIncidence, mCalendar->instances(recurringIncidence)); } else { qCWarning(lcCalDav) << "Cannot find parent of " << toUpload[i]->uid() << "for upload of series."; mFailingUploads.insert(href, "This is an exception occurrence without recurring parent."); continue; } } else { qCWarning(lcCalDav) << "Cannot load series " << toUpload[i]->uid(); mFailingUploads.insert(href, "Database error, cannot load recurring series for the occurrence."); continue; } } else { icsData = IncidenceHandler::toIcs(toUpload[i]); } if (icsData.isEmpty()) { qCDebug(lcCalDav) << "Skipping upload of broken incidence:" << i << ":" << toUpload[i]->uid(); mFailingUploads.insert(href, QByteArray("Cannot generate ICS data.")); } else { qCDebug(lcCalDav) << "Uploading incidence" << i << "via PUT for uid:" << toUpload[i]->uid(); Put *put = new Put(mNetworkManager, mSettings); mRequests.insert(put); connect(put, &Put::finished, this, &NotebookSyncAgent::nonReportRequestFinished); put->sendIcalData(href, icsData, etag); mSentUids.insert(href, toUpload[i]->uid()); } } } void NotebookSyncAgent::nonReportRequestFinished(const QString &uri) { NOTEBOOK_FUNCTION_CALL_TRACE; Request *request = qobject_cast(sender()); if (!request) { setFatal(uri, "Internal nonReportRequestFinished error"); return; } if (request->errorCode() != Buteo::SyncResults::NO_ERROR) { mFailingUploads.insert(uri, request->errorData()); } Put *putRequest = qobject_cast(request); if (putRequest) { if (request->errorCode() == Buteo::SyncResults::NO_ERROR) { const QString &etag = putRequest->updatedETag(uri); if (!etag.isEmpty()) { // Apply Etag and Href changes immediately since incidences are now // for sure on server. updateHrefETag(mSentUids.take(uri), uri, etag); } } else { // Don't try to get etag later for a failed upload. mSentUids.remove(uri); } } Delete *deleteRequest = qobject_cast(request); if (deleteRequest) { if (request->errorCode() != Buteo::SyncResults::NO_ERROR) { // Don't purge yet the locally deleted incidence. KCalendarCore::Incidence::List::Iterator it = mPurgeList.begin(); while (it != mPurgeList.end()) { if (storedIncidenceHrefUri(*it) == uri) { it = mPurgeList.erase(it); } else { ++it; } } } } bool last = true; for (QSet::ConstIterator it = mRequests.constBegin(); it != mRequests.constEnd(); ++it) { last = last && (*it == request || (!qobject_cast(*it) && !qobject_cast(*it))); } if (last && !mSentUids.isEmpty()) { // mSentUids have been cleared from uids that have already // been updated with new etag value. Just remains the ones // that requires additional retrieval to get etag values. sendReportRequest(mSentUids.keys()); } requestFinished(request); } static KCalendarCore::Incidence::List loadAll(mKCal::ExtendedStorage::Ptr storage, mKCal::ExtendedCalendar::Ptr calendar, const KCalendarCore::Incidence::List &incidences) { KCalendarCore::Incidence::List out; for (int i = 0; i < incidences.size(); i++){ if (storage->load(incidences[i]->uid())) { const KCalendarCore::Incidence::Ptr incidence = calendar->incidence(incidences[i]->uid(), incidences[i]->recurrenceId()); if (incidence) { out.append(incidence); } } } return out; } bool NotebookSyncAgent::applyRemoteChanges() { NOTEBOOK_FUNCTION_CALL_TRACE; if (!mNotebook) { qCDebug(lcCalDav) << "Missing notebook in apply changes."; return false; } // mNotebook may not exist in mStorage, because it is new, or // database has been modified and notebooks been reloaded. mKCal::Notebook::Ptr notebook(mStorage->notebook(mNotebook->uid())); if (mEnableDownsync && mNotebookNeedsDeletion) { // delete the notebook from local database if (notebook && !mStorage->deleteNotebook(notebook)) { qCWarning(lcCalDav) << "Cannot delete notebook" << notebook->name() << "from storage."; mNotebookNeedsDeletion = false; } return mNotebookNeedsDeletion; } // If current notebook is not already in storage, we add it. if (!notebook) { if (!mStorage->addNotebook(mNotebook)) { qCDebug(lcCalDav) << "Unable to (re)create notebook" << mNotebook->name() << "for account" << mNotebook->account() << ":" << mRemoteCalendarPath; return false; } notebook = mNotebook; } bool success = true; if ((mEnableDownsync || mSyncMode == SlowSync) && !updateIncidences(mReceivedCalendarResources)) { success = false; } if (mEnableDownsync && !deleteIncidences(mRemoteDeletions)) { success = false; } // Update storage, before possibly changing readOnly flag for this notebook. if (!mStorage->save(mKCal::ExtendedStorage::PurgeDeleted)) { success = false; } if (!mPurgeList.isEmpty() && !mStorage->purgeDeletedIncidences(mPurgeList, notebook->uid())) { // Silently ignore failed purge action in database. qCWarning(lcCalDav) << "Cannot purge from database the marked as deleted incidences."; } notebook->setIsReadOnly(mReadOnlyFlag); notebook->setSyncDate(mNotebookSyncedDateTime); notebook->setName(mNotebook->name()); notebook->setColor(mNotebook->color()); notebook->setSyncProfile(mNotebook->syncProfile()); notebook->setCustomProperty(PATH_PROPERTY, mRemoteCalendarPath); if (!mStorage->updateNotebook(notebook)) { qCWarning(lcCalDav) << "Cannot update notebook" << notebook->name() << "in storage."; success = false; } return success; } Buteo::TargetResults NotebookSyncAgent::result() const { if (mSyncMode == SlowSync) { unsigned int count = 0; for (QList::ConstIterator it = mReceivedCalendarResources.constBegin(); it != mReceivedCalendarResources.constEnd(); ++it) { if (!mFailingUpdates.contains(it->href)) { count += it->incidences.count(); } } return Buteo::TargetResults(mNotebook->name().toHtmlEscaped(), Buteo::ItemCounts(count, 0, 0), Buteo::ItemCounts()); } else { Buteo::TargetResults results(mNotebook->name().toHtmlEscaped()); summarizeResults(&results, LOCAL, Buteo::TargetResults::ITEM_ADDED, mFailingUpdates, mRemoteAdditions); summarizeResults(&results, LOCAL, Buteo::TargetResults::ITEM_DELETED, mFailingUpdates, mRemoteDeletions); summarizeResults(&results, LOCAL, Buteo::TargetResults::ITEM_MODIFIED, mFailingUpdates, mRemoteModifications); summarizeResults(&results, REMOTE, Buteo::TargetResults::ITEM_ADDED, mFailingUploads, mLocalAdditions, mRemoteCalendarPath); summarizeResults(&results, REMOTE, Buteo::TargetResults::ITEM_DELETED, mFailingUploads, mLocalDeletions); summarizeResults(&results, REMOTE, Buteo::TargetResults::ITEM_MODIFIED, mFailingUploads, mLocalModifications); return results; } } void NotebookSyncAgent::requestFinished(Request *request) { NOTEBOOK_FUNCTION_CALL_TRACE; mRequests.remove(request); request->deleteLater(); if (mRequests.isEmpty()) { if (!mSentUids.isEmpty()) { const QList &resources = mReceivedCalendarResources; for (const Reader::CalendarResource &resource : resources) { if (mSentUids.contains(resource.href) && resource.etag.isEmpty()) { // Asked for a resource etag but didn't get it. mFailingUploads.insert(resource.href, QByteArray("Unable to retrieve etag.")); } } } // Flag (or remove flag) for all failing (or not) local changes. flagUploadFailure(mFailingUploads, loadAll(mStorage, mCalendar, mLocalAdditions), mRemoteCalendarPath); flagUploadFailure(mFailingUploads, loadAll(mStorage, mCalendar, mLocalModifications)); emit finished(); } } void NotebookSyncAgent::setFatal(const QString &uri, const QByteArray &errorData) { mFailingUpdates.insert(uri, errorData); mFatalUri = uri; abort(); } void NotebookSyncAgent::finalize() { NOTEBOOK_FUNCTION_CALL_TRACE; } bool NotebookSyncAgent::isFinished() const { return mRequests.isEmpty(); } bool NotebookSyncAgent::isCompleted() const { return mFatalUri.isEmpty(); } bool NotebookSyncAgent::isDeleted() const { return (mEnableDownsync && mNotebookNeedsDeletion); } bool NotebookSyncAgent::hasDownloadErrors() const { return !mFailingUpdates.isEmpty(); } bool NotebookSyncAgent::hasUploadErrors() const { return !mFailingUploads.isEmpty(); } const QString& NotebookSyncAgent::path() const { return mEncodedRemotePath; } // ------------------------------ Utility / implementation functions. // called in the QuickSync codepath after fetching etags for remote resources. // from the etags, we can determine the local and remote sync delta. bool NotebookSyncAgent::calculateDelta( // in parameters: const QHash &remoteUriEtags, // remoteEtags: map of uri to etag which exist on the remote server. // out parameters: KCalendarCore::Incidence::List *localAdditions, KCalendarCore::Incidence::List *localModifications, KCalendarCore::Incidence::List *localDeletions, QSet *remoteChanges, KCalendarCore::Incidence::List *remoteDeletions) { // Note that the mKCal API doesn't provide a way to get all deleted/modified incidences // for a notebook, as it implements the SQL query using an inequality on both modifiedAfter // and createdBefore; so instead we have to build a datetime which "should" satisfy // the inequality for all possible local modifications detectable since the last sync. QDateTime syncDateTime = mNotebook->syncDate().addSecs(1); // deleted after, created before... // load all local incidences KCalendarCore::Incidence::List localIncidences; if (!mStorage->allIncidences(&localIncidences, mNotebook->uid())) { qCWarning(lcCalDav) << "Unable to load notebook incidences, aborting sync of notebook:" << mRemoteCalendarPath << ":" << mNotebook->uid(); return false; } // separate them into buckets. // note that each remote URI can be associated with multiple local incidences (due recurrenceId incidences) // Here we can determine local additions, modifications and remote modifications, deletions. QSet localUris; for (KCalendarCore::Incidence::Ptr incidence : const_cast(localIncidences)) { bool modified = (incidence->created() < syncDateTime && incidence->lastModified() >= syncDateTime); QString remoteUri = storedIncidenceHrefUri(incidence); if (remoteUri.isEmpty()) { remoteUri = createIncidenceHrefUri(incidence, mRemoteCalendarPath); // Imported exceptions don't have URI and etag inherited from parent. if (!incidence->hasRecurrenceId() && remoteUriEtags.contains(remoteUri)) { // we previously upsynced this incidence but then connectivity died and etag was not set. if (!modified) { qCDebug(lcCalDav) << "have previously partially upsynced local addition, needs uri update:" << remoteUri; // Treat it as a remote modification and trigger download for etag and uri update. mUpdatingList.append(incidence); remoteChanges->insert(remoteUri); } else { qCDebug(lcCalDav) << "have local modification to partially synced incidence:" << incidence->uid() << incidence->recurrenceId().toString(); // note: we cannot check the etag to determine if it changed also on server side. // we assume here local modifications only. setIncidenceHrefUri(incidence, remoteUri); setIncidenceETag(incidence, remoteUriEtags.value(remoteUri)); localModifications->append(incidence); } } else if (!isFlagged(incidence) || retryUploadFailure(incidence)) { // it doesn't exist on remote side... new local addition. qCDebug(lcCalDav) << "have new local addition:" << incidence->uid() << incidence->recurrenceId().toString(); localAdditions->append(incidence); } } else { // this is a previously-synced incidence with a remote uri, // OR a newly-added persistent occurrence to a previously-synced recurring series. if (!remoteUriEtags.contains(remoteUri)) { if (!incidenceWithin(incidence, mFromDateTime, mToDateTime)) { qCDebug(lcCalDav) << "ignoring out-of-range missing remote incidence:" << incidence->uid() << incidence->recurrenceId().toString(); } else if (!isFlagged(incidence) || retryDeleteFailure(incidence)) { qCDebug(lcCalDav) << "have remote deletion of previously synced incidence:" << incidence->uid() << incidence->recurrenceId().toString(); // Ignoring local modifications if any. remoteDeletions->append(incidence); } else if (resetDeleteFailure(incidence)) { qCDebug(lcCalDav) << "reset remote deletion:" << incidence->uid() << incidence->recurrenceId().toString(); localAdditions->append(incidence); } } else if (isCopiedDetachedIncidence(incidence)) { if (incidenceETag(incidence) == remoteUriEtags.value(remoteUri)) { qCDebug(lcCalDav) << "Found new locally-added persistent exception:" << incidence->uid() << incidence->recurrenceId().toString() << ":" << remoteUri; localAdditions->append(incidence); } else { qCDebug(lcCalDav) << "ignoring new locally-added persistent exception to remotely modified incidence:" << incidence->uid() << incidence->recurrenceId().toString() << ":" << remoteUri; mUpdatingList.append(incidence); remoteChanges->insert(remoteUri); } } else if (incidenceETag(incidence) != remoteUriEtags.value(remoteUri)) { qCDebug(lcCalDav) << "have remote modification to previously synced incidence at:" << remoteUri; if (!isFlagged(incidence) || retryUpdateFailure(incidence)) { qCDebug(lcCalDav) << "device etag:" << incidenceETag(incidence) << "server etag:" << remoteUriEtags.value(remoteUri); mUpdatingList.append(incidence); // Ignoring local modifications if any. remoteChanges->insert(remoteUri); } else if (resetUpdateFailure(incidence)) { qCDebug(lcCalDav) << "reset remote modification:" << incidence->uid() << incidence->recurrenceId().toString(); setIncidenceETag(incidence, remoteUriEtags.value(remoteUri)); localModifications->append(incidence); } else { qCDebug(lcCalDav) << "ignoring remote modification of flagged incidence:" << incidence->instanceIdentifier(); } } else if (modified) { // this is a real local modification. qCDebug(lcCalDav) << "have local modification:" << incidence->uid() << incidence->recurrenceId().toString(); localModifications->append(incidence); } else if (retryUploadFailure(incidence)) { // this one failed to upload last time, we retry it. qCDebug(lcCalDav) << "have failing to upload incidence:" << incidence->uid() << incidence->recurrenceId().toString(); localModifications->append(incidence); } else if (resetUploadFailure(incidence)) { // scratch previously failing upload with server version. qCDebug(lcCalDav) << "reset failing to upload incidence:" << incidence->uid() << incidence->recurrenceId().toString(); mUpdatingList.append(incidence); remoteChanges->insert(remoteUri); } } localUris.insert(remoteUri); } // List all local deletions reported by mkcal. KCalendarCore::Incidence::List deleted; if (!mStorage->deletedIncidences(&deleted, QDateTime(), mNotebook->uid())) { qCWarning(lcCalDav) << "mKCal::ExtendedStorage::deletedIncidences() failed"; return false; } for (KCalendarCore::Incidence::Ptr incidence : const_cast(deleted)) { QString remoteUri = storedIncidenceHrefUri(incidence); if (remoteUri.isEmpty()) { remoteUri = createIncidenceHrefUri(incidence, mRemoteCalendarPath); if (remoteUriEtags.contains(remoteUri)) { // we originally upsynced this pure-local addition, but then connectivity was // lost before we updated the uid of it locally to include the remote uri. // subsequently, the user deleted the incidence. // Hence, it exists remotely, and has been deleted locally. qCDebug(lcCalDav) << "have local deletion for partially synced incidence:" << incidence->uid() << incidence->recurrenceId().toString(); // We will treat this as a local deletion. setIncidenceHrefUri(incidence, remoteUri); setIncidenceETag(incidence, remoteUriEtags.value(remoteUri)); } } if (remoteUriEtags.contains(remoteUri)) { if (incidenceETag(incidence) == remoteUriEtags.value(remoteUri)) { // the incidence was previously synced successfully. it has now been deleted locally. qCDebug(lcCalDav) << "have local deletion for previously synced incidence:" << incidence->uid() << incidence->recurrenceId().toString(); localDeletions->append(incidence); } else { // Sub-optimal case for persistent exceptions. // TODO: improve handling of this case. qCDebug(lcCalDav) << "ignoring local deletion due to remote modification:" << incidence->uid() << incidence->recurrenceId().toString(); mPurgeList.append(incidence); remoteChanges->insert(remoteUri); } localUris.insert(remoteUri); } else { // it was either already deleted remotely, or was never upsynced from the local prior to deletion. qCDebug(lcCalDav) << "ignoring local deletion of non-existent remote incidence:" << incidence->uid() << incidence->recurrenceId().toString() << "at" << remoteUri; mPurgeList.append(incidence); } } // now determine remote additions. const int nRemoteModifications = remoteChanges->size(); for (const QString &remoteUri : remoteUriEtags.keys()) { if (!localUris.contains(remoteUri)) { qCDebug(lcCalDav) << "have new remote addition:" << remoteUri; remoteChanges->insert(remoteUri); } } qCDebug(lcCalDav) << "Calculated local A/M/R:" << localAdditions->size() << "/" << localModifications->size() << "/" << localDeletions->size(); qCDebug(lcCalDav) << "Calculated remote A/M/R:" << (remoteChanges->size() - nRemoteModifications) << "/" << nRemoteModifications << "/" << remoteDeletions->size(); return true; } static QString nbUid(const QString ¬ebookId, const QString &uid) { return QStringLiteral("NBUID:%1:%2").arg(notebookId).arg(uid); } static KCalendarCore::Incidence::Ptr loadIncidence(mKCal::ExtendedStorage::Ptr storage, mKCal::ExtendedCalendar::Ptr calendar, const QString ¬ebookId, const QString &uid) { const QString &nbuid = nbUid(notebookId, uid); // Load from storage any matching incidence by uid or modified uid. storage->load(uid); storage->load(nbuid); KCalendarCore::Incidence::Ptr incidence = calendar->incidence(uid); if (!incidence) { incidence = calendar->incidence(nbuid); } return incidence; } void NotebookSyncAgent::updateIncidence(KCalendarCore::Incidence::Ptr incidence, KCalendarCore::Incidence::Ptr storedIncidence) { qCDebug(lcCalDav) << "Updating existing event:" << storedIncidence->uid() << storedIncidence->recurrenceId().toString(); storedIncidence->startUpdates(); *storedIncidence.staticCast() = *incidence.staticCast(); flagUpdateSuccess(storedIncidence); storedIncidence->endUpdates(); // Avoid spurious detections of modified incidences // by ensuring that the received last modification date time // is previous to the sync date time. if (storedIncidence->lastModified() > mNotebookSyncedDateTime) { storedIncidence->setLastModified(mNotebookSyncedDateTime.addSecs(-2)); } if (mRemoteChanges.contains(storedIncidenceHrefUri(storedIncidence))) { // Only stores as modifications the incidences that were noted // as remote changes, since we may also update incidences after // push when the etag is not part of the push answer. mRemoteModifications.append(storedIncidence); } } bool NotebookSyncAgent::addIncidence(KCalendarCore::Incidence::Ptr incidence) { qCDebug(lcCalDav) << "Adding new incidence:" << incidence->uid() << incidence->recurrenceId().toString(); mRemoteAdditions.append(incidence); // To avoid spurious appearings of added events when later // calling addedIncidences() and modifiedIncidences(), we // set the creation date and modification date by hand, since // libical has put them to now when they don't exist. if (incidence->created() > mNotebookSyncedDateTime) { incidence->setCreated(mNotebookSyncedDateTime.addSecs(-2)); } if (incidence->lastModified() > mNotebookSyncedDateTime) { incidence->setLastModified(incidence->created()); } // Set-up the default notebook when adding new incidences. mCalendar->addNotebook(mNotebook->uid(), true); if (!mCalendar->setDefaultNotebook(mNotebook->uid())) { qCWarning(lcCalDav) << "Cannot set default notebook to " << mNotebook->uid(); } return mCalendar->addIncidence(incidence); } bool NotebookSyncAgent::addException(KCalendarCore::Incidence::Ptr incidence, KCalendarCore::Incidence::Ptr recurringIncidence, bool ensureRDate) { if (ensureRDate) { const QDateTime lastModified = recurringIncidence->lastModified(); if (recurringIncidence->allDay() && !recurringIncidence->recursOn(incidence->recurrenceId().date(), incidence->recurrenceId().timeZone())) { recurringIncidence->recurrence()->addRDate(incidence->recurrenceId().date()); recurringIncidence->setLastModified(lastModified); } else if (!recurringIncidence->allDay() && !recurringIncidence->recursAt(incidence->recurrenceId())) { recurringIncidence->recurrence()->addRDateTime(incidence->recurrenceId()); recurringIncidence->setLastModified(lastModified); } } return addIncidence(incidence); } bool NotebookSyncAgent::updateIncidences(const QList &resources) { NOTEBOOK_FUNCTION_CALL_TRACE; mRemoteAdditions.clear(); mRemoteModifications.clear(); // We need to coalesce any resources which have the same UID. // This can be the case if there is addition of both a recurring event, // and a modified occurrence of that event, in the same sync cycle. // To ensure that we deal with the original recurring event first, // we find the resource which includes that change and promote it // in the list (so that we deal with it before the other). QList orderedResources; for (int i = resources.count() - 1; i >= 0; --i) { bool prependedResource = false; for (int j = 0; j < resources[i].incidences.count(); ++j) { if (!resources[i].incidences[j]->hasRecurrenceId()) { // we have a non-occurrence event which needs promotion. orderedResources.prepend(resources[i]); prependedResource = true; break; } } if (!prependedResource) { // this resource needs to be appended. orderedResources.append(resources[i]); } } bool success = true; for (int i = 0; i < orderedResources.count(); ++i) { const Reader::CalendarResource &resource = orderedResources.at(i); if (!resource.incidences.size()) { continue; } // Each resource is either a single event series (or non-recurring event) OR // a list of updated/added persistent exceptions to an existing series. // If the resource contains an event series which includes the base incidence, // then we need to compare the local series with the remote series, to ensure // we remove any incidences which occur locally but not remotely. // However, if the resource's incidence list does not contain the base incidence, // but instead contains just persistent exceptions (ie, have recurrenceId) then // we can assume that no persistent exceptions were removed - only added/updated. // find the recurring incidence (parent) in the update list, and save it. // alternatively, it may be a non-recurring base incidence. const QString uid = resource.incidences.first()->uid(); int parentIndex = -1; for (int i = 0; i < resource.incidences.size(); ++i) { if (!resource.incidences[i] || resource.incidences[i]->uid() != uid) { qCWarning(lcCalDav) << "Updated incidence list contains incidences with non-matching uids!"; return false; // this is always an error. each resource corresponds to a single event series. } if (!resource.incidences[i]->hasRecurrenceId()) { parentIndex = i; } updateIncidenceHrefEtag(resource.incidences[i], resource.href, resource.etag); } qCDebug(lcCalDav) << "Saving the added/updated base incidence before saving persistent exceptions:" << uid; KCalendarCore::Incidence::Ptr localBaseIncidence = loadIncidence(mStorage, mCalendar, mNotebook->uid(), uid); if (localBaseIncidence) { if (parentIndex >= 0) { resource.incidences[parentIndex]->setUid(localBaseIncidence->uid()); updateIncidence(resource.incidences[parentIndex], localBaseIncidence); } } else { if (parentIndex == -1) { // construct a recurring parent series for these orphans. localBaseIncidence = KCalendarCore::Incidence::Ptr(resource.incidences.first()->clone()); localBaseIncidence->setRecurrenceId(QDateTime()); } else { localBaseIncidence = resource.incidences[parentIndex]; } localBaseIncidence->setUid(nbUid(mNotebook->uid(), uid)); if (addIncidence(localBaseIncidence)) { localBaseIncidence = loadIncidence(mStorage, mCalendar, mNotebook->uid(), uid); } else { localBaseIncidence = KCalendarCore::Incidence::Ptr(); } } if (!localBaseIncidence) { qCWarning(lcCalDav) << "Error saving base incidence of resource" << resource.href; mFailingUpdates.insert(resource.href, QByteArray("Cannot create local parent.")); success = false; continue; // don't return false and block the entire sync cycle, just ignore this event. } // update persistent exceptions which are in the remote list. QList remoteRecurrenceIds; for (int i = 0; i < resource.incidences.size(); ++i) { KCalendarCore::Incidence::Ptr remoteInstance = resource.incidences[i]; if (!remoteInstance->hasRecurrenceId()) { continue; // already handled this one. } remoteRecurrenceIds.append(remoteInstance->recurrenceId()); qCDebug(lcCalDav) << "Now saving a persistent exception:" << remoteInstance->recurrenceId().toString(); remoteInstance->setUid(localBaseIncidence->uid()); KCalendarCore::Incidence::Ptr localInstance = mCalendar->incidence(remoteInstance->uid(), remoteInstance->recurrenceId()); if (localInstance) { updateIncidence(remoteInstance, localInstance); } else if (!addException(remoteInstance, localBaseIncidence, parentIndex == -1)) { qCWarning(lcCalDav) << "Error saving updated persistent occurrence of resource" << resource.href << ":" << remoteInstance->recurrenceId().toString(); mFailingUpdates.insert(resource.href, QByteArray("Cannot create exception.")); success = false; continue; // don't return false and block the entire sync cycle, just ignore this event. } } // remove persistent exceptions which are not in the remote list. KCalendarCore::Incidence::List localInstances; if (localBaseIncidence->recurs()) localInstances = mCalendar->instances(localBaseIncidence); for (int i = 0; i < localInstances.size(); ++i) { KCalendarCore::Incidence::Ptr localInstance = localInstances[i]; if (!remoteRecurrenceIds.contains(localInstance->recurrenceId())) { qCDebug(lcCalDav) << "Schedule for removal persistent occurrence:" << localInstance->recurrenceId().toString(); // Will be deleted in the call to deleteIncidences mRemoteDeletions.append(localInstance); } } } if (!mFailingUpdates.isEmpty()) { for (int i = 0; i < mUpdatingList.size(); i++){ if (mFailingUpdates.contains(storedIncidenceHrefUri(mUpdatingList[i]))) { const QString uid = mUpdatingList[i]->uid(); const QDateTime recid = mUpdatingList[i]->recurrenceId(); KCalendarCore::Incidence::Ptr incidence = mCalendar->incidence(uid, recid); if (!incidence && mStorage->load(uid)) { incidence = mCalendar->incidence(uid, recid); } if (incidence) { flagUpdateFailure(incidence); } } } } return success; } bool NotebookSyncAgent::deleteIncidences(const KCalendarCore::Incidence::List deletedIncidences) { NOTEBOOK_FUNCTION_CALL_TRACE; bool success = true; for (KCalendarCore::Incidence::Ptr incidence : deletedIncidences) { KCalendarCore::Incidence::Ptr doomed = mCalendar->incidence(incidence->uid(), incidence->recurrenceId()); if (!doomed) { mStorage->load(incidence->uid()); doomed = mCalendar->incidence(incidence->uid(), incidence->recurrenceId()); } if (doomed && !mCalendar->deleteIncidence(doomed)) { qCWarning(lcCalDav) << "Unable to delete incidence: " << doomed->uid() << doomed->recurrenceId().toString(); mFailingUpdates.insert(storedIncidenceHrefUri(doomed), QByteArray("Cannot delete incidence.")); flagDeleteFailure(doomed); success = false; } else { qCDebug(lcCalDav) << "Deleted incidence: " << doomed->uid() << doomed->recurrenceId().toString(); } } return success; } void NotebookSyncAgent::updateHrefETag(const QString &uid, const QString &href, const QString &etag) const { if (!mStorage->load(uid)) { qCWarning(lcCalDav) << "Unable to load incidence from database:" << uid; return; } KCalendarCore::Incidence::Ptr localBaseIncidence = mCalendar->incidence(uid); if (localBaseIncidence) { localBaseIncidence->update(); updateIncidenceHrefEtag(localBaseIncidence, href, etag); localBaseIncidence->updated(); if (localBaseIncidence->recurs()) { const KCalendarCore::Incidence::List instances = mCalendar->instances(localBaseIncidence); for (const KCalendarCore::Incidence::Ptr &instance : instances) { instance->update(); updateIncidenceHrefEtag(instance, href, etag); instance->updated(); } } } else { qCWarning(lcCalDav) << "Unable to find base incidence: " << uid; } } buteo-sync-plugin-caldav-0.3.14/src/notebooksyncagent.h000066400000000000000000000143331467717066200231330ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Bea Lam * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef NOTEBOOKSYNCAGENT_P_H #define NOTEBOOKSYNCAGENT_P_H #include "reader.h" #include #include #include #include class QNetworkAccessManager; class Request; class Settings; class NotebookSyncAgent : public QObject { Q_OBJECT public: enum SyncMode { NoSyncMode, SlowSync, // download everything QuickSync // updates only }; explicit NotebookSyncAgent(mKCal::ExtendedCalendar::Ptr calendar, mKCal::ExtendedStorage::Ptr storage, QNetworkAccessManager *networkAccessManager, Settings *settings, const QString &encodedRemotePath, bool readOnlyFlag = false, QObject *parent = 0); ~NotebookSyncAgent(); bool setNotebookFromInfo(const QString ¬ebookName, const QString &color, const QString &userEmail, bool allowEvents, bool allowTodos, bool allowJournals, const QString &accountId, const QString &pluginName, const QString &syncProfile); void startSync(const QDateTime &fromDateTime, const QDateTime &toDateTime, bool withUpsync, bool withDownsync); void abort(); bool applyRemoteChanges(); Buteo::TargetResults result() const; void finalize(); bool isFinished() const; bool isCompleted() const; bool isDeleted() const; bool hasDownloadErrors() const; bool hasUploadErrors() const; const QString& path() const; signals: void finished(); private slots: void reportRequestFinished(const QString &uri); void nonReportRequestFinished(const QString &uri); void processETags(const QString &uri); private: void sendReportRequest(const QStringList &remoteUris = QStringList()); void requestFinished(Request *request); void setFatal(const QString &uri, const QByteArray &errorData); void fetchRemoteChanges(); bool updateIncidences(const QList &resources); bool deleteIncidences(const KCalendarCore::Incidence::List deletedIncidences); void updateIncidence(KCalendarCore::Incidence::Ptr incidence, KCalendarCore::Incidence::Ptr storedIncidence); bool addIncidence(KCalendarCore::Incidence::Ptr incidence); bool addException(KCalendarCore::Incidence::Ptr incidence, KCalendarCore::Incidence::Ptr recurringIncidence, bool ensureRDate = false); void updateHrefETag(const QString &uid, const QString &href, const QString &etag) const; void sendLocalChanges(); QString constructLocalChangeIcs(KCalendarCore::Incidence::Ptr updatedIncidence); bool calculateDelta(const QHash &remoteUriEtags, KCalendarCore::Incidence::List *localAdditions, KCalendarCore::Incidence::List *localModifications, KCalendarCore::Incidence::List *localDeletions, QSet *remoteChanges, KCalendarCore::Incidence::List *remoteDeletions); QNetworkAccessManager* mNetworkManager; Settings *mSettings; QSet mRequests; mKCal::ExtendedCalendar::Ptr mCalendar; mKCal::ExtendedStorage::Ptr mStorage; mKCal::Notebook::Ptr mNotebook; QDateTime mFromDateTime; QDateTime mToDateTime; QDateTime mNotebookSyncedDateTime; QString mEncodedRemotePath; QString mRemoteCalendarPath; // contains calendar path. resource prefix. doesn't include host, percent decoded. SyncMode mSyncMode; // quick (etag-based delta detection) or slow (full report) sync bool mRetriedReport; // some servers will fail the first request but succeed on second bool mNotebookNeedsDeletion; // if the calendar was deleted remotely, we will need to delete it locally. bool mEnableUpsync, mEnableDownsync; bool mReadOnlyFlag; // these are used only in quick-sync mode. // delta detection and change data KCalendarCore::Incidence::List mLocalAdditions; KCalendarCore::Incidence::List mLocalModifications; KCalendarCore::Incidence::List mLocalDeletions; QSet mRemoteChanges; // Set of URLs to be downloaded KCalendarCore::Incidence::List mRemoteDeletions; KCalendarCore::Incidence::List mRemoteAdditions; KCalendarCore::Incidence::List mRemoteModifications; KCalendarCore::Incidence::List mPurgeList; KCalendarCore::Incidence::List mUpdatingList; // Incidences corresponding to mRemoteModifications QHash mSentUids; // Dictionnary of sent (href, uid) made from // local additions, modifications. QHash mFailingUploads; // List of hrefs with upload errors, with the server response. QHash mFailingUpdates; // List of hrefs from which incidences failed to update. QString mFatalUri; // A key from mFailingUpdates that prevents the sync to complete. // received remote incidence resource data QList mReceivedCalendarResources; friend class tst_NotebookSyncAgent; }; #endif // NOTEBOOKSYNCAGENT_P_H buteo-sync-plugin-caldav-0.3.14/src/propfind.cpp000066400000000000000000000477111467717066200215610ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2019 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Damien Caliste * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "propfind.h" #include "settings.h" #include #include #include #include "logging.h" #define PROP_URI "uri" static bool readResourceType(QXmlStreamReader *reader, bool *isCalendar) { /* e.g.: */ for (; !reader->atEnd(); reader->readNext()) { if (reader->name() == "calendar") { *isCalendar = true; } if (reader->name() == "resourcetype" && reader->isEndElement()) { return true; } } return false; } static bool readPrivilegeSet(QXmlStreamReader *reader, bool *readOnly) { /* e.g.: */ bool readPriv = false; bool writePriv = false; for (; !reader->atEnd(); reader->readNext()) { if (reader->name() == "read") { readPriv = true; } else if (reader->name() == "write") { writePriv = true; } else if (reader->name() == "current-user-privilege-set" && reader->isEndElement()) { *readOnly = readPriv && !writePriv; return true; } } return false; } static bool readComponentSet(QXmlStreamReader *reader, bool *allowEvents, bool *allowTodos, bool *allowJournals) { /* e.g.: */ *allowEvents = false; *allowTodos = false; *allowJournals = false; for (; !reader->atEnd(); reader->readNext()) { if (reader->name() == "comp") { const QStringRef component(reader->attributes().value("name")); if (component == QString::fromLatin1("VEVENT")) *allowEvents = true; if (component == QString::fromLatin1("VTODO")) *allowTodos = true; if (component == QString::fromLatin1("VJOURNAL")) *allowJournals = true; } else if (reader->name() == "supported-calendar-component-set" && reader->isEndElement()) { return true; } } return false; } static bool readCalendarProp(QXmlStreamReader *reader, bool *isCalendar, QString *label, QString *color, QString *userPrincipal, bool *readOnly, bool *allowEvents, bool *allowTodos, bool *allowJournals) { /* e.g.: My events #4887e1ff */ QString displayName; QString displayColor; QString currentUserPrincipal; bool readOnlyStatus = false; *isCalendar = false; *allowEvents = true; *allowTodos = true; *allowJournals = true; for (; !reader->atEnd(); reader->readNext()) { if (reader->name() == "displayname" && reader->isStartElement()) { displayName = reader->readElementText(); } else if (reader->name() == "calendar-color" && reader->isStartElement()) { displayColor = reader->readElementText(); if (displayColor.startsWith("#") && displayColor.length() == 9) { displayColor = displayColor.left(7); } } else if (reader->name() == "current-user-principal" && reader->isStartElement()) { for (;!reader->atEnd(); reader->readNext()) { if (reader->name() == "href" && reader->isStartElement()) { currentUserPrincipal = reader->readElementText(); break; } else if (reader->name() == "current-user-principal" && reader->isEndElement()) { break; } } } else if (reader->name() == "resourcetype" && reader->isStartElement()) { if (!readResourceType(reader, isCalendar)) { return false; } } else if (reader->name() == "current-user-privilege-set" && reader->isStartElement()) { if (!readPrivilegeSet(reader, &readOnlyStatus)) { return false; } } else if (reader->name() == "supported-calendar-component-set" && reader->isStartElement()) { if (!readComponentSet(reader, allowEvents, allowTodos, allowJournals)) { return false; } } else if (reader->name() == "prop" && reader->isEndElement()) { if (*isCalendar) { *label = displayName.isEmpty() ? QStringLiteral("Calendar") : displayName; *color = displayColor; *userPrincipal = currentUserPrincipal; *readOnly = readOnlyStatus; } return true; } } return false; } static bool readCalendarPropStat(QXmlStreamReader *reader, bool *isCalendar, QString *label, QString *color, QString *userPrincipal, bool *readOnly, bool *allowEvents, bool *allowTodos, bool *allowJournals) { /* e.g.: My events #4887e1ff HTTP/1.1 200 OK */ for (; !reader->atEnd(); reader->readNext()) { if (reader->name() == "prop" && reader->isStartElement()) { if (!readCalendarProp(reader, isCalendar, label, color, userPrincipal, readOnly, allowEvents, allowTodos, allowJournals)) { return false; } } else if (reader->name() == "propstat" && reader->isEndElement()) { return true; } } return false; } static bool readCalendarsResponse(QXmlStreamReader *reader, QList *calendars) { /* e.g.: /calendars/username%40server.tld/events-calendar/ My events #4887e1ff HTTP/1.1 200 OK HTTP/1.1 404 Not Found */ bool responseIsCalendar = false; bool hasPropStat = false; PropFind::CalendarInfo calendarInfo; for (; !reader->atEnd(); reader->readNext()) { if (reader->name() == "href" && reader->isStartElement() && calendarInfo.remotePath.isEmpty()) { // The account stores this with the encoding, so we're converting from // percent encoding later. calendarInfo.remotePath = reader->readElementText(); } if (reader->name() == "propstat" && reader->isStartElement()) { bool propStatIsCalendar = false; QString displayname, color, userPrincipal; bool readOnly = false; bool allowEvents = true, allowTodos = true, allowJournals = true; if (!readCalendarPropStat(reader, &propStatIsCalendar, &displayname, &color, &userPrincipal, &readOnly, &allowEvents, &allowTodos, &allowJournals)) { return false; } else if (propStatIsCalendar) { responseIsCalendar = true; calendarInfo.displayName = displayname; calendarInfo.color = color; calendarInfo.userPrincipal = userPrincipal.trimmed(); calendarInfo.readOnly = readOnly; calendarInfo.allowEvents = allowEvents; calendarInfo.allowTodos = allowTodos; calendarInfo.allowJournals = allowJournals; } hasPropStat = true; } if (reader->name() == "response" && reader->isEndElement()) { if (!responseIsCalendar) { return hasPropStat; } if (calendarInfo.remotePath.isEmpty()) { return false; } calendars->append(calendarInfo); return true; } } return false; } static bool readUserAddressSetResponse(QXmlStreamReader *reader, QString *mailtoHref, QString *homeHref) { /* expect a response like: /principals/users/username%40server.tld/ /caldav/ mailto:username@server.tld /principals/users/username%40server.tld/ HTTP/1.1 200 OK */ bool canReadMailtoHref = false; bool canReadHomeHref = false; bool valid = false; for (; !reader->atEnd(); reader->readNext()) { if (reader->name() == "calendar-user-address-set") { canReadMailtoHref = reader->isStartElement(); } else if (reader->name() == "calendar-home-set") { canReadHomeHref = reader->isStartElement(); } else if (canReadMailtoHref && reader->name() == "href" && reader->isStartElement()) { valid = true; QString href = reader->readElementText(); if (href.startsWith(QStringLiteral("mailto:"), Qt::CaseInsensitive)) { *mailtoHref = href.mid(7); // chop off "mailto:" } } else if (canReadHomeHref && reader->name() == "href" && reader->isStartElement()) { valid = true; *homeHref = reader->readElementText(); } else if (reader->name() == "propstat" && reader->isEndElement()) { return valid; } } return false; } static bool readUserPrincipalResponse(QXmlStreamReader *reader, QString *userPrincipal) { /* expect a response like: / /principals/users/username%40server.tld/ HTTP/1.1 200 OK */ QString href; bool canReadUserPrincipalHref = false; for (; !reader->atEnd(); reader->readNext()) { if (reader->name() == "current-user-principal") { if (reader->isStartElement()) { canReadUserPrincipalHref = true; } else if (reader->isEndElement()) { canReadUserPrincipalHref = false; if (href.isEmpty()) { return false; } *userPrincipal = href; return true; } } else if (reader->name() == "href" && reader->isStartElement() && canReadUserPrincipalHref) { href = reader->readElementText(); } } return false; } bool PropFind::parseCalendarResponse(const QByteArray &data) { if (data.isNull() || data.isEmpty()) { return false; } QXmlStreamReader reader(data); reader.setNamespaceProcessing(true); for (; !reader.atEnd(); reader.readNext()) { if (reader.name() == "response" && reader.isStartElement() && !readCalendarsResponse(&reader, &mCalendars)) { return false; } } return true; } bool PropFind::parseUserPrincipalResponse(const QByteArray &data) { if (data.isNull() || data.isEmpty()) { return false; } QXmlStreamReader reader(data); reader.setNamespaceProcessing(true); for (; !reader.atEnd(); reader.readNext()) { if (reader.name() == "response" && reader.isStartElement() && !readUserPrincipalResponse(&reader, &mUserPrincipal)) { return false; } } return true; } bool PropFind::parseUserAddressSetResponse(const QByteArray &data) { if (data.isNull() || data.isEmpty()) { return false; } QXmlStreamReader reader(data); reader.setNamespaceProcessing(true); for (; !reader.atEnd(); reader.readNext()) { if (reader.name() == "response" && reader.isStartElement() && !readUserAddressSetResponse(&reader, &mUserMailtoHref, &mUserHomeHref)) { return false; } } return true; } PropFind::PropFind(QNetworkAccessManager *manager, Settings *settings, QObject *parent) : Request(manager, settings, "PROPFIND", parent) { FUNCTION_CALL_TRACE(lcCalDavTrace); } void PropFind::listCalendars(const QString &calendarsPath) { QByteArray requestData("" \ " " \ " " \ " " \ " " \ " " \ " " \ " " \ " " \ ""); mCalendars.clear(); sendRequest(calendarsPath, requestData, ListCalendars); } void PropFind::listUserAddressSet(const QString &userPrincipal) { const QByteArray requestData(QByteArrayLiteral( "" " " " " " " " " "" )); mUserMailtoHref.clear(); mUserHomeHref.clear(); sendRequest(userPrincipal, requestData, UserAddressSet); } void PropFind::listCurrentUserPrincipal() { const QByteArray requestData(QByteArrayLiteral( "" " " " " " " "" )); mUserPrincipal.clear(); const QString &rootPath = mSettings->davRootPath(); sendRequest(rootPath.isEmpty() ? QStringLiteral("/") : rootPath, requestData, UserPrincipal); } void PropFind::sendRequest(const QString &remotePath, const QByteArray &requestData, PropFindRequestType reqType) { FUNCTION_CALL_TRACE(lcCalDavTrace); mPropFindRequestType = reqType; QNetworkRequest request; prepareRequest(&request, remotePath); if (reqType == ListCalendars) request.setRawHeader("Depth", "1"); else request.setRawHeader("Depth", "0"); request.setRawHeader("Prefer", "return-minimal"); request.setHeader(QNetworkRequest::ContentLengthHeader, requestData.length()); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml; charset=utf-8"); QBuffer *buffer = new QBuffer(this); buffer->setData(requestData); // TODO: when Qt5.8 is available, remove the use of buffer, and pass requestData directly. QNetworkReply *reply = mNAManager->sendCustomRequest(request, REQUEST_TYPE.toLatin1(), buffer); reply->setProperty(PROP_URI, remotePath); debugRequest(request, buffer->buffer()); connect(reply, SIGNAL(finished()), this, SLOT(requestFinished())); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(slotSslErrors(QList))); } void PropFind::handleReply(QNetworkReply *reply) { FUNCTION_CALL_TRACE(lcCalDavTrace); const QString &uri = reply->property(PROP_URI).toString(); if (reply->error() != QNetworkReply::NoError) { finishedWithReplyResult(uri, reply); return; } QByteArray data = reply->readAll(); debugReply(*reply, data); bool success = false; switch (mPropFindRequestType) { case (UserPrincipal): success = parseUserPrincipalResponse(data); break; case (UserAddressSet): success = parseUserAddressSetResponse(data); break; case (ListCalendars): success = parseCalendarResponse(data); break; } if (success) { finishedWithSuccess(uri); } else { finishedWithError(uri, Buteo::SyncResults::INTERNAL_ERROR, QString("Cannot parse response body for PROPFIND"), data); } } const QList& PropFind::calendars() const { return mCalendars; } QString PropFind::userPrincipal() const { return mUserPrincipal; } QString PropFind::userMailtoHref() const { return mUserMailtoHref; } QString PropFind::userHomeHref() const { return mUserHomeHref; } buteo-sync-plugin-caldav-0.3.14/src/propfind.h000066400000000000000000000062401467717066200212160ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2019 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Damien Caliste * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef PROPFIND_H #define PROPFIND_H #include "request.h" class QNetworkAccessManager; class Settings; class PropFind : public Request { Q_OBJECT public: struct CalendarInfo { QString remotePath; QString displayName; QString color; QString userPrincipal; bool readOnly = false; bool allowEvents = true; bool allowTodos = true; bool allowJournals = true; CalendarInfo() {}; CalendarInfo(const QString &path, const QString &name, const QString &color, const QString &principal = QString(), bool readOnly = false) : remotePath(path), displayName(name), color(color) , userPrincipal(principal), readOnly(readOnly) {}; bool operator==(const CalendarInfo &other) const { return (remotePath == other.remotePath && displayName == other.displayName && color == other.color && userPrincipal == other.userPrincipal && readOnly == other.readOnly && allowEvents == other.allowEvents && allowTodos == other.allowTodos && allowJournals == other.allowJournals); } }; explicit PropFind(QNetworkAccessManager *manager, Settings *settings, QObject *parent = 0); void listCurrentUserPrincipal(); QString userPrincipal() const; void listUserAddressSet(const QString &userPrincipal); QString userMailtoHref() const; QString userHomeHref() const; void listCalendars(const QString &calendarsPath); const QList& calendars() const; protected: virtual void handleReply(QNetworkReply *reply); private: enum PropFindRequestType { UserPrincipal, UserAddressSet, ListCalendars }; void sendRequest(const QString &remotePath, const QByteArray &requestData, PropFindRequestType reqType); bool parseUserPrincipalResponse(const QByteArray &data); bool parseUserAddressSetResponse(const QByteArray &data); bool parseCalendarResponse(const QByteArray &data); QList mCalendars; QString mUserPrincipal; QString mUserMailtoHref; QString mUserHomeHref; PropFindRequestType mPropFindRequestType = UserPrincipal; friend class tst_Propfind; }; #endif buteo-sync-plugin-caldav-0.3.14/src/put.cpp000066400000000000000000000067131467717066200205450ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "put.h" #include "report.h" #include "settings.h" #include #include #include #include #include #include #include "logging.h" #define PROP_INCIDENCE_URI "uri" Put::Put(QNetworkAccessManager *manager, Settings *settings, QObject *parent) : Request(manager, settings, "PUT", parent) { } void Put::sendIcalData(const QString &uri, const QString &icalData, const QString &eTag) { FUNCTION_CALL_TRACE(lcCalDavTrace); if (uri.isEmpty()) { finishedWithInternalError("no uri provided"); return; } if (mLocalUriList.contains(uri)) { finishedWithInternalError("already uploaded ical data to uri"); return; } mLocalUriList.insert(uri); QByteArray data = icalData.toUtf8(); if (data.isEmpty()) { finishedWithInternalError("no ical data provided or cannot convert data to UTF-8"); return; } QNetworkRequest request; prepareRequest(&request, uri); if (eTag.isEmpty()) { request.setRawHeader("If-None-Match", "*"); } else { request.setRawHeader("If-Match", eTag.toLatin1()); } request.setHeader(QNetworkRequest::ContentLengthHeader, data.length()); request.setHeader(QNetworkRequest::ContentTypeHeader, "text/calendar; charset=utf-8"); QNetworkReply *reply = mNAManager->put(request, data); reply->setProperty(PROP_INCIDENCE_URI, uri); debugRequest(request, data); connect(reply, SIGNAL(finished()), this, SLOT(requestFinished())); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(slotSslErrors(QList))); } void Put::handleReply(QNetworkReply *reply) { FUNCTION_CALL_TRACE(lcCalDavTrace); // If the put was denied by server (e.g. read-only calendar), the etag // is not updated, so NotebookSyncAgent::finalizeSendingLocalChanges() // will emit a rollback report for this incidence. const QString &uri = reply->property(PROP_INCIDENCE_URI).toString(); if (reply->error() != QNetworkReply::ContentOperationNotPermittedError) { // Server may update the etag as soon as the modification is received and send back a new etag for (const QNetworkReply::RawHeaderPair &header : reply->rawHeaderPairs()) { if (header.first.toLower() == QByteArray("etag")) { mUpdatedETags.insert(uri, header.second); } } } mLocalUriList.remove(uri); finishedWithReplyResult(uri, reply); } QString Put::updatedETag(const QString &uri) const { return mUpdatedETags.value(uri, QString()); } buteo-sync-plugin-caldav-0.3.14/src/put.h000066400000000000000000000030021467717066200201760ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef PUT_H #define PUT_H #include "request.h" #include #include #include #include class QNetworkAccessManager; class Settings; class Put : public Request { Q_OBJECT public: explicit Put(QNetworkAccessManager *manager, Settings *settings, QObject *parent = 0); void sendIcalData(const QString &uri, const QString &icalData, const QString &eTag = QString()); QString updatedETag(const QString &uri) const; protected: virtual void handleReply(QNetworkReply *reply); private: QSet mLocalUriList; QHash mUpdatedETags; }; #endif // PUT_H buteo-sync-plugin-caldav-0.3.14/src/reader.cpp000066400000000000000000000262561467717066200212030ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "reader.h" #include #include #include #include #include #include #include #include #include #include #include "logging.h" namespace { /* Some servers don't XML-escape the ics content when they return it * in the XML stream, so we need to fix any issues. * Note that this can cause line-lengths to exceed the spec (due to * & -> & expansion etc) but our iCal parser is more robust than * our XML parser, so this works. */ QByteArray xmlSanitiseIcsData(const QByteArray &data) { QList lines = data.split('\n'); int depth = 0; bool inCData = false; QByteArray retn; retn.reserve(data.size()); for (QList::const_iterator it = lines.constBegin(); it != lines.constEnd(); it++) { QByteArray line = *it; if (line.contains("BEGIN:VCALENDAR")) { depth += 1; inCData = line.contains(" 0 && !inCData) { // We're inside a VCALENDAR/ics block. // First, hack to turn sanitised input into malformed input: line.replace("&", "&"); line.replace(""", "\""); line.replace("'", "'"); line.replace("<", "<"); line.replace(">", ">"); // Then, fix for malformed input: QString lineStr(line); // RegExp should avoid escaping & when this character is starting // a valid numeric character reference (decimal or hexadecimal). // Other HTLML entities like   seems to make iCal parser // fails, so we're encoding them. lineStr.replace(QRegExp("&(?!#[0-9]+;|#x[0-9A-Fa-f]+;)"), "&"); line = lineStr.toUtf8(); line.replace('"', """); line.replace('\'', "'"); line.replace('<', "<"); line.replace('>', ">"); } retn.append(line); retn.append('\n'); } return retn; } QString ensureUidInVEvent(const QString &data) { // Ensure UID is in VEVENT section for single-event VCALENDAR blobs. int eventCount = 0; // a value of 1 specifies that we should use the fixed data. QStringList fixed; QString storedUidLine; const char separator = '\n'; QStringList original = data.split(separator); bool inVEventSection = false; for (QStringList::const_iterator it = original.constBegin(); it != original.constEnd(); it++) { const QString &line(*it); if (line.startsWith("END:VEVENT")) { inVEventSection = false; } else if (line.startsWith("BEGIN:VEVENT")) { ++eventCount; inVEventSection = true; fixed.append(line); if (!storedUidLine.isEmpty()) { fixed.append(storedUidLine); qCDebug(lcCalDav) << "The UID was before VEVENT data! Report a bug to the application that generated this file."; continue; // BEGIN:VEVENT line already appended } eventCount = -1; break; // use original iCalData if got to VEVENT without finding UID } else if (line.startsWith("UID")) { if (!inVEventSection) { storedUidLine = line; continue; // do not append UID line yet } } fixed.append(line); } // if we found exactly one event and were able to set its UID, return the fixed data. // otherwise, return the original data. return eventCount == 1 ? fixed.join(separator) : data; } QString preprocessIcsData(const QString &data) { QString temp = data.trimmed(); temp.replace(QStringLiteral("\r\n"), QStringLiteral("\n")); temp.replace(QStringLiteral("\n"), QStringLiteral("\r\n")); temp = temp.append(QStringLiteral("\r\n\r\n")); temp = ensureUidInVEvent(temp); return temp; } QString ensureICalVersion(const QString &data) { // Add VERSION:2.0 after the VCALENDAR tag to force iCal parsing. const char separator = '\n'; QStringList original = data.split(separator); QStringList fixed; for (QStringList::const_iterator it = original.constBegin(); it != original.constEnd(); it++) { const QString &line(*it); fixed.append(line); if (line.startsWith("BEGIN:VCALENDAR")) { fixed.append(QStringLiteral("VERSION:2.0\r")); } } return fixed.join(separator); } } Reader::Reader(QObject *parent) : QObject(parent) , mReader(0) , mValidResponse(false) { } Reader::~Reader() { delete mReader; } void Reader::read(const QByteArray &data) { delete mReader; mReader = new QXmlStreamReader(xmlSanitiseIcsData(data)); while (mReader->readNextStartElement()) { if (mReader->name() == "multistatus") { mValidResponse = true; readMultiStatus(); } } } bool Reader::hasError() const { if (!mReader) return false; return !mValidResponse; } const QList& Reader::results() const { return mResults; } void Reader::readMultiStatus() { while (mReader->readNextStartElement()) { if (mReader->name() == "response") { readResponse(); } else { mReader->skipCurrentElement(); } } } void Reader::readResponse() { CalendarResource resource; while (mReader->readNextStartElement()) { if (mReader->name() == "href") { resource.href = QUrl::fromPercentEncoding(mReader->readElementText().toLatin1()); } else if (mReader->name() == "propstat") { readPropStat(resource); } else { mReader->skipCurrentElement(); } } if (resource.href.isEmpty()) { qCWarning(lcCalDav) << "Ignoring received calendar object data, is missing href value"; return; } if (!resource.iCalData.trimmed().isEmpty()) { bool parsed = true; QString icsData = preprocessIcsData(resource.iCalData); KCalendarCore::ICalFormat iCalFormat; KCalendarCore::MemoryCalendar::Ptr cal(new KCalendarCore::MemoryCalendar(QTimeZone::utc())); if (!iCalFormat.fromString(cal, icsData)) { if (iCalFormat.exception() && iCalFormat.exception()->code() == KCalendarCore::Exception::CalVersion1) { KCalendarCore::VCalFormat vCalFormat; if (!vCalFormat.fromString(cal, icsData)) { qCWarning(lcCalDav) << "unable to parse vCal data"; parsed = false; } } else if (iCalFormat.exception() && (iCalFormat.exception()->code() == KCalendarCore::Exception::CalVersionUnknown || iCalFormat.exception()->code() == KCalendarCore::Exception::VersionPropertyMissing)) { iCalFormat.setException(0); qCWarning(lcCalDav) << "unknown or missing version, trying iCal 2.0"; icsData = ensureICalVersion(icsData); if (!iCalFormat.fromString(cal, icsData)) { qCWarning(lcCalDav) << "unable to parse iCal data, returning" << (iCalFormat.exception() ? iCalFormat.exception()->code() : -1); parsed = false; } } else { qCWarning(lcCalDav) << "unable to parse iCal data, returning" << (iCalFormat.exception() ? iCalFormat.exception()->code() : -1); parsed = false; } } if (parsed) { const KCalendarCore::Incidence::List incidences = cal->incidences(); qCDebug(lcCalDav) << "iCal data contains" << incidences.count() << " incidences"; if (incidences.count()) { QString uid = incidences.first()->uid(); // In case of more than one incidence, it contains some // recurring event information, with exception / RECURRENCE-ID defined. for (const KCalendarCore::Incidence::Ptr &incidence : incidences) { if (incidence->uid() != uid) { qCWarning(lcCalDav) << "iCal data contains invalid incidences with conflicting uids"; uid.clear(); break; } } if (!uid.isEmpty()) { for (const KCalendarCore::Incidence::Ptr &incidence : incidences) { if (incidence->type() == KCalendarCore::IncidenceBase::TypeEvent || incidence->type() == KCalendarCore::IncidenceBase::TypeTodo) resource.incidences.append(incidence); } } qCDebug(lcCalDav) << "parsed" << resource.incidences.count() << "events or todos from the iCal data"; } else { qCWarning(lcCalDav) << "iCal data doesn't contain a valid incidence"; } } } mResults.append(resource); } void Reader::readPropStat(CalendarResource &resource) { while (mReader->readNextStartElement()) { if (mReader->name() == "prop") { readProp(resource); } else if (mReader->name() == "status") { resource.status = mReader->readElementText(); } else { mReader->skipCurrentElement(); } } } void Reader::readProp(CalendarResource &resource) { while (mReader->readNextStartElement()) { if (mReader->name() == "getetag") { resource.etag = mReader->readElementText(); } else if (mReader->name() == "calendar-data") { resource.iCalData = mReader->readElementText(QXmlStreamReader::IncludeChildElements); } else { mReader->skipCurrentElement(); } } } buteo-sync-plugin-caldav-0.3.14/src/reader.h000066400000000000000000000032211467717066200206330ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef READER_H #define READER_H #include #include class QXmlStreamReader; class Reader : public QObject { Q_OBJECT public: struct CalendarResource { QString href; QString etag; QString status; QString iCalData; KCalendarCore::Incidence::List incidences; }; explicit Reader(QObject *parent = 0); ~Reader(); void read(const QByteArray &data); bool hasError() const; const QList& results() const; private: void readMultiStatus(); void readResponse(); void readPropStat(CalendarResource &resource); void readProp(CalendarResource &resource); private: QXmlStreamReader *mReader; bool mValidResponse; QList mResults; }; #endif // READER_H buteo-sync-plugin-caldav-0.3.14/src/report.cpp000066400000000000000000000150361467717066200212460ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * Stephan Rave * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "report.h" #include "reader.h" #include "settings.h" #include #include #include #include #include "logging.h" #define PROP_URI "uri" static const QString DateTimeFormat = QStringLiteral("yyyyMMddTHHmmss"); static const QString DateTimeFormatUTC = DateTimeFormat + QStringLiteral("Z"); static QString dateTimeToString(const QDateTime &dt) { if (dt.timeSpec() == Qt::UTC) { return QLocale::c().toString(dt, DateTimeFormatUTC); } else { return QLocale::c().toString(dt, DateTimeFormat); } } static QByteArray timeRangeFilterXml(const QDateTime &fromDateTime, const QDateTime &toDateTime) { QByteArray xml; if (fromDateTime.isValid() || toDateTime.isValid()) { xml = " " \ "" \ ""; if (getCalendarData) { requestData += \ ""; } requestData += \ "" "" \ ""; if (fromDateTime.isValid() || toDateTime.isValid()) { requestData.append(timeRangeFilterXml(fromDateTime, toDateTime)); } requestData += \ "" \ "" \ ""; sendRequest(remoteCalendarPath, requestData); } void Report::multiGetEvents(const QString &remoteCalendarPath, const QStringList &eventHrefList) { FUNCTION_CALL_TRACE(lcCalDavTrace); if (eventHrefList.isEmpty()) { return; } QByteArray requestData = "" \ ""; for (const QString &eventHref : eventHrefList) { requestData.append(""); requestData.append(eventHref.toUtf8()); requestData.append(""); } requestData.append(""); sendRequest(remoteCalendarPath, requestData); mFetchedUris = eventHrefList; } void Report::sendRequest(const QString &remoteCalendarPath, const QByteArray &requestData) { FUNCTION_CALL_TRACE(lcCalDavTrace); mRemoteCalendarPath = remoteCalendarPath; QNetworkRequest request; prepareRequest(&request, remoteCalendarPath); request.setRawHeader("Depth", "1"); request.setRawHeader("Prefer", "return-minimal"); request.setHeader(QNetworkRequest::ContentLengthHeader, requestData.length()); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml; charset=utf-8"); QBuffer *buffer = new QBuffer(this); buffer->setData(requestData); // TODO: when Qt5.8 is available, remove the use of buffer, and pass requestData directly. QNetworkReply *reply = mNAManager->sendCustomRequest(request, REQUEST_TYPE.toLatin1(), buffer); reply->setProperty(PROP_URI, remoteCalendarPath); debugRequest(request, buffer->buffer()); connect(reply, SIGNAL(finished()), this, SLOT(requestFinished())); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(slotSslErrors(QList))); } void Report::handleReply(QNetworkReply *reply) { FUNCTION_CALL_TRACE(lcCalDavTrace); const QString &uri = reply->property(PROP_URI).toString(); if (reply->error() != QNetworkReply::NoError) { finishedWithReplyResult(uri, reply); return; } const QByteArray data = reply->readAll(); debugReply(*reply, data); if (!data.isNull() && !data.isEmpty()) { Reader reader; reader.read(data); if (reader.hasError()) { finishedWithError(uri, Buteo::SyncResults::INTERNAL_ERROR, QString("Malformed response body for REPORT"), data); } else { mReceivedResources = reader.results(); finishedWithSuccess(uri); } } else { finishedWithError(uri, Buteo::SyncResults::INTERNAL_ERROR, QString("Empty response body for REPORT"), QByteArray()); } } const QList& Report::receivedCalendarResources() const { return mReceivedResources; } const QStringList& Report::fetchedUris() const { return mFetchedUris; } buteo-sync-plugin-caldav-0.3.14/src/report.h000066400000000000000000000043771467717066200207210ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef REPORT_H #define REPORT_H #include "request.h" #include "reader.h" #include #include class QNetworkAccessManager; class Settings; class Report : public Request { Q_OBJECT public: explicit Report(QNetworkAccessManager *manager, Settings *settings, QObject *parent = 0); void getAllEvents(const QString &remoteCalendarPath, const QDateTime &fromDateTime = QDateTime(), const QDateTime &toDateTime = QDateTime()); void getAllETags(const QString &remoteCalendarPath, const QDateTime &fromDateTime = QDateTime(), const QDateTime &toDateTime = QDateTime()); void multiGetEvents(const QString &remoteCalendarPath, const QStringList &eventHrefList); const QList& receivedCalendarResources() const; const QStringList& fetchedUris() const; protected: virtual void handleReply(QNetworkReply *reply); private: void sendRequest(const QString &remoteCalendarPath, const QByteArray &requestData); void sendCalendarQuery(const QString &remoteCalendarPath, const QDateTime &fromDateTime, const QDateTime &toDateTime, bool getCalendarData); QString mRemoteCalendarPath; QStringList mFetchedUris; QList mReceivedResources; }; #endif // REPORT_H buteo-sync-plugin-caldav-0.3.14/src/request.cpp000066400000000000000000000177021467717066200214250ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Bea Lam * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "request.h" #include "logging.h" Request::Request(QNetworkAccessManager *manager, Settings *settings, const QString &requestType, QObject *parent) : QObject(parent) , mNAManager(manager) , REQUEST_TYPE(requestType) , mSettings(settings) , mNetworkError(QNetworkReply::NoError) , mMinorCode(Buteo::SyncResults::NO_ERROR) { FUNCTION_CALL_TRACE(lcCalDavTrace); mSelfPointer = this; } Buteo::SyncResults::MinorCode Request::errorCode() const { return mMinorCode; } QString Request::errorMessage() const { return mErrorMessage; } QByteArray Request::errorData() const { return mErrorData; } QNetworkReply::NetworkError Request::networkError() const { return mNetworkError; } QString Request::command() const { return REQUEST_TYPE; } void Request::finishedWithReplyResult(const QString &uri, QNetworkReply *reply) { mNetworkError = reply->error(); if (reply->error() == QNetworkReply::NoError) { debugReplyAndReadAll(reply); finishedWithSuccess(uri); } else if (reply->error() == QNetworkReply::ContentOperationNotPermittedError) { // Gracefully continue when the operation fails for permission // reasons (like pushing to a read-only resource). qCDebug(lcCalDav) << "The" << command() << "operation requested on the remote content is not permitted"; debugReplyAndReadAll(reply); finishedWithSuccess(uri); } else { Buteo::SyncResults::MinorCode errorCode = Buteo::SyncResults::CONNECTION_ERROR; if (reply->error() == QNetworkReply::SslHandshakeFailedError || reply->error() == QNetworkReply::ContentAccessDenied || reply->error() == QNetworkReply::AuthenticationRequiredError) { errorCode = Buteo::SyncResults::AUTHENTICATION_FAILURE; } qCWarning(lcCalDav) << "The" << command() << "operation failed with error:" << reply->error(); const QByteArray data(reply->readAll()); debugReply(*reply, data); finishedWithError(uri, errorCode, QString("Network request failed with QNetworkReply::NetworkError: %1").arg(reply->error()), data); } } void Request::slotSslErrors(QList errors) { QNetworkReply *reply = qobject_cast(sender()); if (!reply) { return; } debugReplyAndReadAll(reply); if (mSettings->ignoreSSLErrors()) { qCDebug(lcCalDav) << "Ignoring SSL error response"; reply->ignoreSslErrors(errors); } else { qCWarning(lcCalDav) << command() << "request received SSL error response!"; } } void Request::requestFinished() { FUNCTION_CALL_TRACE(lcCalDavTrace); if (wasDeleted()) { qCDebug(lcCalDav) << command() << "request was aborted"; return; } QNetworkReply *reply = qobject_cast(sender()); if (!reply) { finishedWithInternalError(QString()); return; } reply->deleteLater(); qCDebug(lcCalDav) << command() << "request finished:" << reply->error(); handleReply(reply); } void Request::finishedWithError(const QString &uri, Buteo::SyncResults::MinorCode minorCode, const QString &errorString, const QByteArray &errorData) { if (minorCode != Buteo::SyncResults::NO_ERROR) { qCWarning(lcCalDav) << REQUEST_TYPE << "request failed." << minorCode << errorString; } mMinorCode = minorCode; mErrorMessage = errorString; mErrorData = errorData; emit finished(uri); } void Request::finishedWithInternalError(const QString &uri, const QString &errorString) { finishedWithError(uri, Buteo::SyncResults::INTERNAL_ERROR, errorString.isEmpty() ? QStringLiteral("Internal error") : errorString, QByteArray()); } void Request::finishedWithSuccess(const QString &uri) { mMinorCode = Buteo::SyncResults::NO_ERROR; emit finished(uri); } void Request::prepareRequest(QNetworkRequest *request, const QString &requestPath) { QUrl url(mSettings->serverAddress()); if (!mSettings->authToken().isEmpty()) { request->setRawHeader(QString("Authorization").toLatin1(), QString("Bearer " + mSettings->authToken()).toLatin1()); } else { url.setUserName(mSettings->username()); url.setPassword(mSettings->password()); } url.setPath(requestPath); request->setUrl(url); } bool Request::wasDeleted() const { return mSelfPointer == 0; } void Request::debugRequest(const QNetworkRequest &request, const QByteArray &data) { const QStringList lines = debuggingString(request, data).split('\n', QString::SkipEmptyParts); for (QString line : lines) { qCDebug(lcCalDavProtocol) << line.replace('\r', ' '); } } void Request::debugRequest(const QNetworkRequest &request, const QString &data) { const QStringList lines = debuggingString(request, data.toUtf8()).split('\n', QString::SkipEmptyParts); for (QString line : lines) { qCDebug(lcCalDavProtocol) << line.replace('\r', ' '); } } void Request::debugReply(const QNetworkReply &reply, const QByteArray &data) { const QStringList lines = debuggingString(reply, data).split('\n', QString::SkipEmptyParts); for (QString line : lines) { qCDebug(lcCalDavProtocol) << line.replace('\r', ' '); } } void Request::debugReplyAndReadAll(QNetworkReply *reply) { const QStringList lines = debuggingString(*reply, reply->readAll()).split('\n', QString::SkipEmptyParts); for (QString line : lines) { qCDebug(lcCalDavProtocol) << line.replace('\r', ' '); } } QString Request::debuggingString(const QNetworkRequest &request, const QByteArray &data) { QStringList text; text += "---------------------------------------------------------------------"; const QList &rawHeaderList = request.rawHeaderList(); for (const QByteArray &rawHeader : rawHeaderList) { text += rawHeader + " : " + request.rawHeader(rawHeader); } QUrl censoredUrl = request.url(); censoredUrl.setUserName(QStringLiteral("user")); censoredUrl.setPassword(QStringLiteral("pass")); text += "URL = " + censoredUrl.toString(); text += "Request : " + REQUEST_TYPE + "\n" + data; text += "---------------------------------------------------------------------\n"; return text.join(QChar('\n')); } QString Request::debuggingString(const QNetworkReply &reply, const QByteArray &data) { QStringList text; text += "---------------------------------------------------------------------"; text += REQUEST_TYPE + " response status code: " + reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toString(); const QList headers = reply.rawHeaderPairs(); text += REQUEST_TYPE + " response headers:"; for (const QNetworkReply::RawHeaderPair header : headers) { text += "\t" + header.first + " : " + header.second; } if (!data.isEmpty()) { text += REQUEST_TYPE + " response data:" + data; } text += "---------------------------------------------------------------------\n"; return text.join(QChar('\n')); } buteo-sync-plugin-caldav-0.3.14/src/request.h000066400000000000000000000057051467717066200210720ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef REQUEST_H #define REQUEST_H #include "settings.h" #include #include #include #include #include #include #include class Request : public QObject { Q_OBJECT public: explicit Request(QNetworkAccessManager *manager, Settings *settings, const QString &requestType, QObject *parent = 0); QString command() const; Buteo::SyncResults::MinorCode errorCode() const; QString errorMessage() const; QByteArray errorData() const; QNetworkReply::NetworkError networkError() const; Q_SIGNALS: void finished(const QString &uri); protected Q_SLOTS: virtual void slotSslErrors(QList); void requestFinished(); protected: void prepareRequest(QNetworkRequest *request, const QString &requestPath); virtual void handleReply(QNetworkReply *reply) = 0; bool wasDeleted() const; void finishedWithSuccess(const QString &uri); void finishedWithError(const QString &uri, Buteo::SyncResults::MinorCode minorCode, const QString &errorMessage, const QByteArray &errorData); void finishedWithInternalError(const QString &uri, const QString &errorString = QString()); void finishedWithReplyResult(const QString &uri, QNetworkReply *reply); void debugRequest(const QNetworkRequest &request, const QByteArray &data); void debugRequest(const QNetworkRequest &request, const QString &data); void debugReply(const QNetworkReply &reply, const QByteArray &data); void debugReplyAndReadAll(QNetworkReply *reply); QString debuggingString(const QNetworkRequest &request, const QByteArray &data); QString debuggingString(const QNetworkReply &reply, const QByteArray &data); QNetworkAccessManager *mNAManager; const QString REQUEST_TYPE; Settings* mSettings; QPointer mSelfPointer; QNetworkReply::NetworkError mNetworkError; Buteo::SyncResults::MinorCode mMinorCode; QString mErrorMessage; QByteArray mErrorData; }; #endif // REQUEST_H buteo-sync-plugin-caldav-0.3.14/src/settings.cpp000066400000000000000000000042031467717066200215650ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "settings.h" Settings::Settings() : mIgnoreSSLErrors(false) { } QString Settings::authToken() const { return mOAuthToken; } void Settings::setAuthToken(const QString & token) { mOAuthToken = token; } bool Settings::ignoreSSLErrors() const { return mIgnoreSSLErrors; } void Settings::setIgnoreSSLErrors(bool ignore) { mIgnoreSSLErrors = ignore; } QString Settings::password() const { return mPassword; } void Settings::setPassword(const QString & password) { mPassword = password; } QString Settings::username() const { return mUsername; } void Settings::setUsername(const QString & username) { mUsername = username; } void Settings::setServerAddress(const QString &serverAddress) { mServerAddress = serverAddress; } QString Settings::serverAddress() const { return mServerAddress; } void Settings::setDavRootPath(const QString &path) { mDavRootPath = path; } QString Settings::davRootPath() const { return mDavRootPath; } void Settings::setUserPrincipal(const QString &href) { mUserPrincipal = href; } QString Settings::userPrincipal() const { return mUserPrincipal; } void Settings::setUserMailtoHref(const QString &href) { mUserMailtoHref = href; } QString Settings::userMailtoHref() const { return mUserMailtoHref; } buteo-sync-plugin-caldav-0.3.14/src/settings.h000066400000000000000000000035261467717066200212410ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-caldav package * * Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Mani Chandrasekar * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef SETTINGS_H #define SETTINGS_H #include #include class Settings { public: Settings(); QString authToken() const; void setAuthToken(const QString &token); void setUsername(const QString &username); QString username() const; void setPassword(const QString &password); QString password() const; void setIgnoreSSLErrors(bool ignore); bool ignoreSSLErrors() const; void setServerAddress(const QString &serverAddress); QString serverAddress() const; void setDavRootPath(const QString &path); QString davRootPath() const; void setUserPrincipal(const QString &href); QString userPrincipal() const; void setUserMailtoHref(const QString &href); QString userMailtoHref() const; private: QString mUserPrincipal; QString mUserMailtoHref; QString mServerAddress; QString mDavRootPath; QString mOAuthToken; QString mUsername; QString mPassword; bool mIgnoreSSLErrors; }; #endif // SETTINGS_H buteo-sync-plugin-caldav-0.3.14/src/src.pri000066400000000000000000000021421467717066200205240ustar00rootroot00000000000000QT -= gui QT += network dbus CONFIG += link_pkgconfig console PKGCONFIG += buteosyncfw5 libsignon-qt5 accounts-qt5 libsailfishkeyprovider PKGCONFIG += signon-oauth2plugin KF5CalendarCore libmkcal-qt5 INCLUDEPATH += $$PWD SOURCES += \ $$PWD/caldavclient.cpp \ $$PWD/report.cpp \ $$PWD/put.cpp \ $$PWD/delete.cpp \ $$PWD/propfind.cpp \ $$PWD/reader.cpp \ $$PWD/settings.cpp \ $$PWD/request.cpp \ $$PWD/authhandler.cpp \ $$PWD/incidencehandler.cpp \ $$PWD/notebooksyncagent.cpp \ $$PWD/logging.cpp HEADERS += \ $$PWD/caldavclient.h \ $$PWD/buteo-caldav-plugin.h \ $$PWD/report.h \ $$PWD/put.h \ $$PWD/delete.h \ $$PWD/propfind.h \ $$PWD/reader.h \ $$PWD/settings.h \ $$PWD/request.h \ $$PWD/authhandler.h \ $$PWD/incidencehandler.h \ $$PWD/notebooksyncagent.h \ $$PWD/logging.h OTHER_FILES += \ $$PWD/xmls/client/caldav.xml \ $$PWD/xmls/sync/caldav-sync.xml MOC_DIR=$$PWD/.moc/ OBJECTS_DIR=$$PWD/.obj/ buteo-sync-plugin-caldav-0.3.14/src/src.pro000066400000000000000000000007111467717066200205320ustar00rootroot00000000000000TARGET = caldav-client include(src.pri) VER_MAJ = 0 VER_MIN = 1 VER_PAT = 0 QMAKE_CXXFLAGS += -Wall \ -g \ -Wno-cast-align \ -O2 -finline-functions DEFINES += BUTEOCALDAVPLUGIN_LIBRARY TEMPLATE = lib CONFIG += plugin target.path = $$[QT_INSTALL_LIBS]/buteo-plugins-qt5/oopp sync.path = /etc/buteo/profiles/sync sync.files = xmls/sync/* client.path = /etc/buteo/profiles/client client.files = xmls/client/* INSTALLS += target sync client buteo-sync-plugin-caldav-0.3.14/src/xmls/000077500000000000000000000000001467717066200202055ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/src/xmls/client/000077500000000000000000000000001467717066200214635ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/src/xmls/client/caldav.xml000066400000000000000000000003511467717066200234360ustar00rootroot00000000000000 buteo-sync-plugin-caldav-0.3.14/src/xmls/sync/000077500000000000000000000000001467717066200211615ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/src/xmls/sync/caldav-sync.xml000066400000000000000000000014631467717066200241130ustar00rootroot00000000000000 buteo-sync-plugin-caldav-0.3.14/tests/000077500000000000000000000000001467717066200175755ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/tests/caldavclient/000077500000000000000000000000001467717066200222265ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/tests/caldavclient/caldavclient.pro000066400000000000000000000003321467717066200253770ustar00rootroot00000000000000TEMPLATE = app TARGET = tst_caldavclient QT += testlib QT -= gui CONFIG += debug include($$PWD/../../src/src.pri) SOURCES += tst_caldavclient.cpp target.path = /opt/tests/buteo/plugins/caldav/ INSTALLS += target buteo-sync-plugin-caldav-0.3.14/tests/caldavclient/tst_caldavclient.cpp000066400000000000000000000207721467717066200262650ustar00rootroot00000000000000/* -*- c-basic-offset: 4 -*- */ /* * Copyright (C) 2020 Caliste Damien. * Contact: Damien Caliste * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include "caldavclient.h" #include #include class tst_CalDavClient : public QObject { Q_OBJECT public: tst_CalDavClient(); virtual ~tst_CalDavClient(); public slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); private slots: void initConfig(); void initConfigWithSettingsInAccount(); void addInitCalendars(); void loadAccountCalendars(); void mergeAccountCalendars(); void removeAccountCalendar(); private: Accounts::Manager* mManager; Accounts::Account* mAccount; Buteo::SyncProfile mProfile; }; tst_CalDavClient::tst_CalDavClient() : mManager(nullptr) , mAccount(nullptr) , mProfile(QLatin1String("test_profile")) { } tst_CalDavClient::~tst_CalDavClient() { } static const QString SERVER_ADDRESS = QLatin1String("https://example.org"); static const QString WEBDAV_PATH = QLatin1String("/dav/calendar"); void tst_CalDavClient::initTestCase() { // Create a fake account mManager = new Accounts::Manager; QVERIFY(mManager->provider(QLatin1String("onlinesync")).isValid()); mAccount = mManager->createAccount(QLatin1String("onlinesync")); QVERIFY(mAccount); mAccount->setEnabled(true); QVERIFY(mAccount->supportsService(QLatin1String("caldav"))); Accounts::Service srv = mAccount->services(QLatin1String("caldav")).first(); mAccount->selectService(srv); mAccount->setValue("caldav-sync/profile_id", mProfile.name()); mAccount->setValue("server_address", SERVER_ADDRESS); mAccount->setValue("ignore_ssl_errors", true); mAccount->setCredentialsId(1); mAccount->setEnabled(true); QVERIFY(mAccount->syncAndBlock()); QVERIFY(mAccount->id() > 0); mProfile.setKey(Buteo::KEY_ACCOUNT_ID, QString::number(mAccount->id())); } void tst_CalDavClient::cleanupTestCase() { mAccount->remove(); QVERIFY(mAccount->syncAndBlock()); delete mAccount; delete mManager; } void tst_CalDavClient::init() { } void tst_CalDavClient::cleanup() { } void tst_CalDavClient::initConfig() { // profile argument is copied at construction time. CalDavClient client(QLatin1String("caldav"), mProfile, nullptr); QVERIFY(client.init()); QVERIFY(client.mService); QCOMPARE(client.mService->account()->id(), mAccount->id()); QCOMPARE(client.mSettings.serverAddress(), SERVER_ADDRESS); QVERIFY(client.mSettings.ignoreSSLErrors()); } void tst_CalDavClient::initConfigWithSettingsInAccount() { Accounts::Account* account; Buteo::SyncProfile profile(QLatin1String("test_profile_at_root")); // Create a fake account with settings defined in the account. QVERIFY(mManager->provider(QLatin1String("onlinesync")).isValid()); account = mManager->createAccount(QLatin1String("onlinesync")); QVERIFY(account); account->setEnabled(true); account->setValue("server_address", SERVER_ADDRESS); account->setValue("webdav_path", WEBDAV_PATH); account->setValue("ignore_ssl_errors", true); QVERIFY(account->supportsService(QLatin1String("caldav"))); Accounts::Service srv = account->services(QLatin1String("caldav")).first(); account->selectService(srv); account->setValue("caldav-sync/profile_id", profile.name()); account->setCredentialsId(1); account->setEnabled(true); QVERIFY(account->syncAndBlock()); QVERIFY(account->id() > 0); profile.setKey(Buteo::KEY_ACCOUNT_ID, QString::number(account->id())); CalDavClient client(QLatin1String("caldav"), profile, nullptr); QVERIFY(client.init()); QVERIFY(client.mService); QCOMPARE(client.mService->account()->id(), account->id()); QCOMPARE(client.mSettings.serverAddress(), SERVER_ADDRESS); QCOMPARE(client.mSettings.davRootPath(), WEBDAV_PATH); QVERIFY(client.mSettings.ignoreSSLErrors()); account->remove(); QVERIFY(account->syncAndBlock()); } void tst_CalDavClient::addInitCalendars() { mAccount->setValue("calendars", QStringList() << QLatin1String("/foo/") << QLatin1String("/bar%40plop/")); mAccount->setValue("enabled_calendars", QStringList() << QLatin1String("/bar%40plop/")); mAccount->setValue("calendar_display_names", QStringList() << QLatin1String("Foo") << QLatin1String("Bar")); mAccount->setValue("calendar_colors", QStringList() << QLatin1String("#FF0000") << QLatin1String("#00FF00")); QVERIFY(mAccount->syncAndBlock()); } void tst_CalDavClient::loadAccountCalendars() { CalDavClient client(QLatin1String("caldav"), mProfile, nullptr); QVERIFY(client.init()); const QList &calendars = client.loadAccountCalendars(); QCOMPARE(calendars.count(), 1); QCOMPARE(calendars.first().remotePath, QLatin1String("/bar%40plop/")); QCOMPARE(calendars.first().displayName, QLatin1String("Bar")); QCOMPARE(calendars.first().color, QLatin1String("#00FF00")); QVERIFY(calendars.first().userPrincipal.isEmpty()); } void tst_CalDavClient::mergeAccountCalendars() { CalDavClient client(QLatin1String("caldav"), mProfile, nullptr); client.mManager = mManager; // So we can share the same Account pointers. QVERIFY(client.init()); QList remoteCalendars; remoteCalendars << PropFind::CalendarInfo{QLatin1String("/bar%40plop/"), QLatin1String("Bar"), QLatin1String("#0000FF"), QLatin1String("/principals/2")}; remoteCalendars << PropFind::CalendarInfo{QLatin1String("/foo/"), QLatin1String("New foo"), QLatin1String("#FF0000"), QString()}; remoteCalendars << PropFind::CalendarInfo{QLatin1String("/toto%40tutu/"), QLatin1String("Toto"), QLatin1String("#FF00FF"), QString()}; const QList &calendars = client.mergeAccountCalendars(remoteCalendars); QCOMPARE(calendars.count(), 2); QCOMPARE(calendars[0].remotePath, QLatin1String("/bar%40plop/")); QCOMPARE(calendars[0].displayName, QLatin1String("Bar")); QCOMPARE(calendars[0].color, QLatin1String("#0000FF")); QCOMPARE(calendars[0].userPrincipal, QLatin1String("/principals/2")); QCOMPARE(calendars[1].remotePath, QLatin1String("/toto%40tutu/")); QCOMPARE(calendars[1].displayName, QLatin1String("Toto")); QCOMPARE(calendars[1].color, QLatin1String("#FF00FF")); QVERIFY(calendars[1].userPrincipal.isEmpty()); // Also check that account has updated the disabled calendar. Accounts::Service srv = mAccount->services(QLatin1String("caldav")).first(); mAccount->selectService(srv); const QStringList &allCalendars = mAccount->value("calendars").toStringList(); QCOMPARE(allCalendars.count(), 3); QVERIFY(allCalendars.contains(QLatin1String("/foo/"))); const QStringList &names = mAccount->value("calendar_display_names").toStringList(); int at = allCalendars.indexOf(QLatin1String("/foo/")); QVERIFY(at < names.length()); QCOMPARE(names[at], QLatin1String("New foo")); } void tst_CalDavClient::removeAccountCalendar() { CalDavClient client(QLatin1String("caldav"), mProfile, nullptr); client.mManager = mManager; // So we can share the same Account pointers. QVERIFY(client.init()); client.removeAccountCalendars(QStringList() << QLatin1String("/bar%40plop/") << QLatin1String("/notStoredOne/")); Accounts::Service srv = mAccount->services(QLatin1String("caldav")).first(); mAccount->selectService(srv); const QStringList &allCalendars = mAccount->value("calendars").toStringList(); QCOMPARE(allCalendars.count(), 2); QVERIFY(!allCalendars.contains(QLatin1String("/bar%40plop/"))); const QStringList &names = mAccount->value("calendar_display_names").toStringList(); QCOMPARE(names.count(), 2); QVERIFY(!names.contains(QLatin1String("Bar"))); } #include "tst_caldavclient.moc" QTEST_MAIN(tst_CalDavClient) buteo-sync-plugin-caldav-0.3.14/tests/notebooksyncagent/000077500000000000000000000000001467717066200233315ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/tests/notebooksyncagent/data/000077500000000000000000000000001467717066200242425ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/tests/notebooksyncagent/data/notebooksyncagent_insert_and_update.xml000066400000000000000000000034111467717066200342670ustar00rootroot00000000000000 /remote.php/dav/calendars/benjamin/privatbenjamin/1416147359.R649.ics "78f7040fc1d488f57e1ea7a551c70d14" BEGIN:VCALENDAR PRODID:-//K Desktop Environment//NONSGML libkcal 4.3//EN VERSION:2.0 X-KDE-ICAL-IMPLEMENTATION-VERSION:1.0 BEGIN:VEVENT DTSTAMP:20150915T040805Z CREATED:20150821T211132Z UID:7d145c8e-0f34-45a0-b8ca-d9c86093bc12 SEQUENCE:1 LAST-MODIFIED:20150915T040805Z SUMMARY:My Event CATEGORIES:CatA,CatB,Persönlich RRULE:FREQ=YEARLY;BYMONTHDAY=9;BYMONTH=11 DTSTART;VALUE=DATE:20121109 DTEND;VALUE=DATE:20121110 TRANSP:TRANSPARENT END:VEVENT END:VCALENDAR HTTP/1.1 200 OK /remote.php/dav/calendars/benjamin/privatbenjamin/1416147706.R693.ics "7b5b64da5d4b7e6d5dd7a011298333a5" BEGIN:VCALENDAR PRODID:-//K Desktop Environment//NONSGML libkcal 4.3//EN VERSION:2.0 X-KDE-ICAL-IMPLEMENTATION-VERSION:1.0 BEGIN:VEVENT DTSTAMP:20160416T164159Z CREATED:20160413T003504Z UID:7d145c8e-0f34-45a0-b8ca-d9c86093bc12 SEQUENCE:1 LAST-MODIFIED:20160416T164159Z SUMMARY:My Event 2 CATEGORIES:CatA,CatB,Persönlich RRULE:FREQ=YEARLY;BYMONTHDAY=9;BYMONTH=11 DTSTART;VALUE=DATE:20121109 DTEND;VALUE=DATE:20121110 TRANSP:TRANSPARENT END:VEVENT END:VCALENDAR HTTP/1.1 200 OK buteo-sync-plugin-caldav-0.3.14/tests/notebooksyncagent/data/notebooksyncagent_insert_exdate.xml000066400000000000000000000022641467717066200334420ustar00rootroot00000000000000 /calendars/9e3e9495-7fca-46b1-9ae4-207f3a1a9148.ics "78f7040fc1d488f57e1ea7a551c70d14" BEGIN:VCALENDAR PRODID:-//K Desktop Environment//NONSGML libkcal 4.3//EN VERSION:2.0 BEGIN:VEVENT DTSTAMP:20191126T111854Z CREATED:20191126T095627Z UID:9e3e9495-7fca-46b1-9ae4-207f3a1a9148 LAST-MODIFIED:20191126T111854Z SUMMARY:EXAMPLE RECURRENCE-ID;VALUE=DATE:20190724 DTSTART;TZID=Europe/Vienna:20190724T133000 DTEND;TZID=Europe/Vienna:20190724T143000 TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT DTSTAMP:20191126T095627Z CREATED:20190724T075519Z UID:9e3e9495-7fca-46b1-9ae4-207f3a1a9148 LAST-MODIFIED:20191126T095627Z EXDATE;VALUE=DATE:20190724 RDATE;VALUE=DATE:20190724 DTSTART;VALUE=DATE:20190724 DTEND;VALUE=DATE:20190725 TRANSP:OPAQUE END:VEVENT END:VCALENDAR HTTP/1.1 200 OK buteo-sync-plugin-caldav-0.3.14/tests/notebooksyncagent/data/notebooksyncagent_orphanexceptions.xml000066400000000000000000000070021467717066200341700ustar00rootroot00000000000000 /remote.php/dav/calendars/dupluser/personal/aaaaaaaaaa.ics "aaaaaaaaaa1" BEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VEVENT LAST-MODIFIED:20170522T064817Z DTSTAMP:20170522T064817Z UID:aaaaaaaaaa SUMMARY:Test a DTSTART:20170322T110000Z DTEND:20170322T120000Z DESCRIPTION;LANGUAGE=en-US:Test a description CLASS:PUBLIC TRANSP:OPAQUE LOCATION;LANGUAGE=en-US:@office + GTM END:VEVENT END:VCALENDAR HTTP/1.1 200 OK /remote.php/dav/calendars/dupluser/personal/d1158dac-5a63-49d5-83b8-176bb792a088_20170323T110000.ics "d1158dac5a6349d583b8176bb792a08820170323T1100001" BEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VEVENT LAST-MODIFIED:20170522T064817Z DTSTAMP:20170522T064817Z UID:d1158dac-5a63-49d5-83b8-176bb792a088 SUMMARY:Test orphan one PRIORITY:5 STATUS:CONFIRMED RECURRENCE-ID:20170323T110000Z ORGANIZER;CN=Test Guy;SCHEDULE-AGENT=CLIENT:mailto:testguy@merproject.org RELATED-TO;RELTYPE=SIBLING:9DFE2850-C84A-4623-80D5-81C1B9974F52 DTSTART:20170323T110000Z DTEND:20170323T120000Z DESCRIPTION;LANGUAGE=en-US:Test orphan one description CLASS:PUBLIC TRANSP:OPAQUE SEQUENCE:32 LOCATION;LANGUAGE=en-US:@office + GTM END:VEVENT END:VCALENDAR HTTP/1.1 200 OK /remote.php/dav/calendars/dupluser/personal/bbbbbbbbbb.ics "bbbbbbbbbb1" BEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VEVENT LAST-MODIFIED:20170522T064817Z DTSTAMP:20170522T064817Z UID:bbbbbbbbbb SUMMARY:Test b DTSTART:20170324T110000Z DTEND:20170324T120000Z DESCRIPTION;LANGUAGE=en-US:Test b description CLASS:PUBLIC TRANSP:OPAQUE LOCATION;LANGUAGE=en-US:@office + GTM END:VEVENT END:VCALENDAR HTTP/1.1 200 OK /remote.php/dav/calendars/dupluser/personal/d1158dac-5a63-49d5-83b8-176bb792a088_20170330T110000.ics "d1158dac5a6349d583b8176bb792a08820170330T1100001" BEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VEVENT LAST-MODIFIED:20170522T064817Z DTSTAMP:20170522T064817Z UID:d1158dac-5a63-49d5-83b8-176bb792a088 SUMMARY:Test orphan two PRIORITY:5 STATUS:CONFIRMED RECURRENCE-ID:20170330T110000Z ORGANIZER;CN=Test Guy;SCHEDULE-AGENT=CLIENT:mailto:testguy@merproject.org RELATED-TO;RELTYPE=SIBLING:9DFE2850-C84A-4623-80D5-81C1B9974F52 DTSTART:20170330T110000Z DTEND:20170330T120000Z DESCRIPTION;LANGUAGE=en-US:Test orphan two description CLASS:PUBLIC TRANSP:OPAQUE SEQUENCE:33 LOCATION;LANGUAGE=en-US:@office + GTM END:VEVENT END:VCALENDAR HTTP/1.1 200 OK buteo-sync-plugin-caldav-0.3.14/tests/notebooksyncagent/data/notebooksyncagent_recurring.xml000066400000000000000000000022041467717066200325760ustar00rootroot00000000000000 /remote.php/dav/calendars/dcaliste ++/Top calendar %20/7d145c8e-0f34-45a0-b8ca-d9c86093bc11.ics BEGIN:VCALENDAR PRODID:-//K Desktop Environment//NONSGML libkcal 4.3//EN VERSION:2.0 X-KDE-ICAL-IMPLEMENTATION-VERSION:1.0 BEGIN:VEVENT DTSTAMP:20160524T190438Z CREATED:20160520T195542Z UID:7d145c8e-0f34-45a0-b8ca-d9c86093bc11 SEQUENCE:1 LAST-MODIFIED:20160524T190438Z SUMMARY:My Event CATEGORIES:CatA,CatB,Persönlich RRULE:FREQ=YEARLY;BYMONTHDAY=9;BYMONTH=11 DTSTART;VALUE=DATE:20121109T100000Z DTEND;VALUE=DATE:20121110T110000Z TRANSP:TRANSPARENT END:VEVENT BEGIN:VEVENT UID:7d145c8e-0f34-45a0-b8ca-d9c86093bc11 SEQUENCE:2 LAST-MODIFIED:20160524T190438Z SUMMARY:My Event CATEGORIES:CatA,CatB,Persönlich RRULE:FREQ=YEARLY;BYMONTHDAY=9;BYMONTH=11 RECURRENCE-ID;RANGE=THISANDFUTURE:20121109T100000Z DTSTART;VALUE=DATE:20121109T110000Z DTEND;VALUE=DATE:20121110T120000Z END:VEVENT END:VCALENDAR buteo-sync-plugin-caldav-0.3.14/tests/notebooksyncagent/data/notebooksyncagent_simple.xml000066400000000000000000000014031467717066200320670ustar00rootroot00000000000000 /user/cal.ics/ BEGIN:VCALENDAR PRODID:-//Radicale//NONSGML Radicale Server//EN VERSION:2.0 BEGIN:VEVENT DTSTAMP:20160930T132609Z CREATED:20160930T132609Z X-RADICALE-NAME:972a7c13-bbd6-4fce-9ebb-03a808111828.ics UID:972a7c13-bbd6-4fce-9ebb-03a808111828 LAST-MODIFIED:20160930T132609Z SUMMARY:Test DTSTART;TZID=Europe/Paris:20160930T160000 DTEND;TZID=Europe/Paris:20160930T170000 TRANSP:OPAQUE BEGIN:VALARM ACTION: TRIGGER;VALUE=DURATION:-PT30M X-KDE-KCALCORE-ENABLED:TRUE END:VALARM END:VEVENT END:VCALENDAR buteo-sync-plugin-caldav-0.3.14/tests/notebooksyncagent/notebooksyncagent.pro000066400000000000000000000005351467717066200276120ustar00rootroot00000000000000TEMPLATE = app TARGET = tst_notebooksyncagent QT += testlib QT -= gui CONFIG += debug include($$PWD/../../src/src.pri) SOURCES += tst_notebooksyncagent.cpp OTHER_FILES += data/*xml datafiles.files += data/*xml datafiles.path = /opt/tests/buteo/plugins/caldav/data/ target.path = /opt/tests/buteo/plugins/caldav/ INSTALLS += target datafiles buteo-sync-plugin-caldav-0.3.14/tests/notebooksyncagent/tst_notebooksyncagent.cpp000066400000000000000000001316631467717066200304750ustar00rootroot00000000000000/* -*- c-basic-offset: 4 -*- */ /* * Copyright (C) 2016-2021 Caliste Damien. * Contact: Damien Caliste * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include "incidencehandler.h" #include "report.h" #include "put.h" #include "logging.h" #include #include #include #include #include #include #include #include #include #include class tst_NotebookSyncAgent : public QObject { Q_OBJECT public: tst_NotebookSyncAgent(); virtual ~tst_NotebookSyncAgent(); public slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); private slots: void insertEvent_data(); void insertEvent(); void insertMultipleEvents_data(); void insertMultipleEvents(); void updateEvent(); void updateHrefETag(); void calculateDelta(); void oneDownSyncCycle_data(); void oneDownSyncCycle(); void oneUpSyncCycle_data(); void oneUpSyncCycle(); void updateIncidence_data(); void updateIncidence(); void requestFinished(); void result(); private: Settings m_settings; NotebookSyncAgent *m_agent; typedef QMap IncidenceDescr; }; tst_NotebookSyncAgent::tst_NotebookSyncAgent() : m_agent(0) { } tst_NotebookSyncAgent::~tst_NotebookSyncAgent() { } void tst_NotebookSyncAgent::initTestCase() { if (qgetenv("SQLITESTORAGEDB").isEmpty()) { qputenv("SQLITESTORAGEDB", "./db"); QFile::remove("./db"); } } void tst_NotebookSyncAgent::cleanupTestCase() { } void tst_NotebookSyncAgent::init() { mKCal::ExtendedCalendar::Ptr cal = mKCal::ExtendedCalendar::Ptr(new mKCal::ExtendedCalendar( QByteArray( "UTC" ) )); mKCal::ExtendedStorage::Ptr store = mKCal::ExtendedCalendar::defaultStorage(cal); store->open(); QNetworkAccessManager *mNAManager = new QNetworkAccessManager(); m_agent = new NotebookSyncAgent(cal, store, mNAManager, &m_settings, QLatin1String("/testCal/")); mKCal::Notebook *notebook = new mKCal::Notebook("123456789", "test1", "test 1", "red", true, false, false, false, false); m_agent->mNotebook = mKCal::Notebook::Ptr(notebook); store->addNotebook(m_agent->mNotebook); } void tst_NotebookSyncAgent::cleanup() { m_agent->mStorage->save(); mKCal::Notebook::Ptr notebook = m_agent->mStorage->notebook("123456789"); m_agent->mStorage->deleteNotebook(notebook); m_agent->mStorage->close(); delete m_agent->mNetworkManager; delete m_agent; } void tst_NotebookSyncAgent::insertEvent_data() { QTest::addColumn("xmlFilename"); QTest::addColumn("expectedUID"); QTest::addColumn("expectedSummary"); QTest::addColumn("expectedRecurrenceID"); QTest::addColumn("expectedNAlarms"); QTest::newRow("simple event response") << "data/notebooksyncagent_simple.xml" << QStringLiteral("NBUID:123456789:972a7c13-bbd6-4fce-9ebb-03a808111828") << QStringLiteral("Test") << QString() << 1; QTest::newRow("recurring event response") << "data/notebooksyncagent_recurring.xml" << QStringLiteral("NBUID:123456789:7d145c8e-0f34-45a0-b8ca-d9c86093bc11") << QStringLiteral("My Event") << QStringLiteral("2012-11-09T10:00:00Z") << 0; QTest::newRow("insert and update event response") << "data/notebooksyncagent_insert_and_update.xml" << QStringLiteral("NBUID:123456789:7d145c8e-0f34-45a0-b8ca-d9c86093bc12") << QStringLiteral("My Event 2") << QString() << 0; QTest::newRow("insert an exception at an EXDATE") << "data/notebooksyncagent_insert_exdate.xml" << QStringLiteral("NBUID:123456789:9e3e9495-7fca-46b1-9ae4-207f3a1a9148") << QStringLiteral("EXAMPLE") << QStringLiteral("2019-07-24") << 0; } void tst_NotebookSyncAgent::insertEvent() { QFETCH(QString, xmlFilename); QFETCH(QString, expectedUID); QFETCH(QString, expectedSummary); QFETCH(QString, expectedRecurrenceID); QFETCH(int, expectedNAlarms); QFile f(QStringLiteral("%1/%2").arg(QCoreApplication::applicationDirPath(), xmlFilename)); if (!f.exists() || !f.open(QIODevice::ReadOnly)) { QFAIL("Data file does not exist or cannot be opened for reading!"); } Reader rd; rd.read(f.readAll()); QVERIFY(m_agent->updateIncidences(rd.results())); KCalendarCore::Incidence::Ptr ev; if (expectedRecurrenceID.isEmpty()) ev = m_agent->mCalendar->event(expectedUID); else ev = m_agent->mCalendar->event(expectedUID, QDateTime::fromString(expectedRecurrenceID, Qt::ISODate)); QVERIFY(ev); QCOMPARE(ev->uid(), expectedUID); QCOMPARE(ev->summary(), expectedSummary); QCOMPARE(ev->alarms().length(), expectedNAlarms); } void tst_NotebookSyncAgent::insertMultipleEvents_data() { QTest::addColumn("xmlFilename"); QTest::addColumn("expectedUIDs"); QTest::addColumn("expectedSummaries"); QTest::addColumn("expectedRecurrenceIDs"); QTest::newRow("singleA, orphan1, singleB, orphan2") << "data/notebooksyncagent_orphanexceptions.xml" << (QStringList() << QStringLiteral("NBUID:123456789:aaaaaaaaaa") << QStringLiteral("NBUID:123456789:d1158dac-5a63-49d5-83b8-176bb792a088") << QStringLiteral("NBUID:123456789:bbbbbbbbbb") << QStringLiteral("NBUID:123456789:d1158dac-5a63-49d5-83b8-176bb792a088")) << (QStringList() << QStringLiteral("Test a") << QStringLiteral("Test orphan one") << QStringLiteral("Test b") << QStringLiteral("Test orphan two")) << (QStringList() << QString() << QStringLiteral("2017-03-23T11:00:00Z") << QString() << QStringLiteral("2017-03-30T11:00:00Z")); } void tst_NotebookSyncAgent::insertMultipleEvents() { QFETCH(QString, xmlFilename); QFETCH(QStringList, expectedUIDs); QFETCH(QStringList, expectedSummaries); QFETCH(QStringList, expectedRecurrenceIDs); QVERIFY(expectedUIDs.size() == expectedSummaries.size()); QVERIFY(expectedSummaries.size() == expectedRecurrenceIDs.size()); QFile f(QStringLiteral("%1/%2").arg(QCoreApplication::applicationDirPath(), xmlFilename)); if (!f.exists() || !f.open(QIODevice::ReadOnly)) { QFAIL("Data file does not exist or cannot be opened for reading!"); } Reader rd; rd.read(f.readAll()); QVERIFY(m_agent->updateIncidences(rd.results())); KCalendarCore::Incidence::List incidences = m_agent->mCalendar->incidences(); for (int i = 0; i < expectedUIDs.size(); ++i) { KCalendarCore::Incidence::Ptr ev; if (expectedRecurrenceIDs[i].isEmpty()) { ev = m_agent->mCalendar->event(expectedUIDs[i]); } else { QDateTime recId = QDateTime::fromString(expectedRecurrenceIDs[i], Qt::ISODate); ev = m_agent->mCalendar->event(expectedUIDs[i], recId); } qCDebug(lcCalDav) << "Trying to find event:" << expectedUIDs[i] << expectedRecurrenceIDs[i]; QVERIFY(ev); QCOMPARE(ev->uid(), expectedUIDs[i]); QCOMPARE(ev->summary(), expectedSummaries[i]); } } static QString fetchUri(KCalendarCore::Incidence::Ptr incidence) { Q_FOREACH (const QString &comment, incidence->comments()) { if (comment.startsWith("buteo:caldav:uri:")) { QString uri = comment.mid(17); return uri; } } return QString(); } static QString fetchETag(KCalendarCore::Incidence::Ptr incidence) { const QStringList &comments(incidence->comments()); Q_FOREACH (const QString &comment, comments) { if (comment.startsWith("buteo:caldav:etag:")) { return comment.mid(18); } } return QString(); } void tst_NotebookSyncAgent::updateEvent() { // Populate the database. KCalendarCore::Incidence::Ptr incidence = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); incidence->setUid("123456-moz"); incidence->setNonKDECustomProperty("X-MOZ-LASTACK", "20171013T174424Z"); incidence->setCreated(QDateTime(QDate(2019, 03, 28), QTime(), Qt::UTC)); QCOMPARE(incidence->created().date(), QDate(2019, 03, 28)); QVERIFY(m_agent->mCalendar->addEvent(incidence.staticCast(), m_agent->mNotebook->uid())); m_agent->mStorage->save(); // Test that event exists. incidence = m_agent->mCalendar->event(QStringLiteral("123456-moz")); QVERIFY(incidence); QCOMPARE(incidence->customProperties().count(), 1); QCOMPARE(incidence->nonKDECustomProperty("X-MOZ-LASTACK"), QStringLiteral("20171013T174424Z")); QCOMPARE(incidence->created().date(), QDate(2019, 03, 28)); // Update event with a custom property. KCalendarCore::Incidence::Ptr update = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); update->setUid("123456-moz"); update->setNonKDECustomProperty("X-MOZ-LASTACK", "20171016T174424Z"); update->setAllDay(false); update->addComment(QStringLiteral("buteo:caldav:uri:plop.ics")); m_agent->updateIncidence(update, incidence); // Check that custom property is updated as well. incidence = m_agent->mCalendar->event(QStringLiteral("123456-moz")); QVERIFY(incidence); QCOMPARE(incidence->customProperties().count(), 1); QCOMPARE(incidence->nonKDECustomProperty("X-MOZ-LASTACK"), QStringLiteral("20171016T174424Z")); } void tst_NotebookSyncAgent::updateHrefETag() { // Populate the database. KCalendarCore::Incidence::Ptr incidence = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); incidence->setUid("123456-single"); m_agent->mCalendar->addEvent(incidence.staticCast(), m_agent->mNotebook->uid()); incidence = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); incidence->setUid("123456-recurs"); incidence->setDtStart(QDateTime::currentDateTimeUtc()); incidence->recurrence()->setDaily(1); incidence->recurrence()->setDuration(28); m_agent->mCalendar->addEvent(incidence.staticCast(), m_agent->mNotebook->uid()); QDateTime refId = incidence->recurrence()->getNextDateTime(incidence->dtStart().addDays(4)); incidence = m_agent->mCalendar->dissociateSingleOccurrence(incidence, refId); m_agent->mCalendar->addEvent(incidence.staticCast(), m_agent->mNotebook->uid()); m_agent->mStorage->save(); // Simple case. m_agent->updateHrefETag(QStringLiteral("123456-single"), QStringLiteral("/testCal/123456-single.ics"), QStringLiteral("\"123456\"")); incidence = m_agent->mCalendar->event(QStringLiteral("123456-single")); QCOMPARE(fetchUri(incidence), QStringLiteral("/testCal/123456-single.ics")); QCOMPARE(fetchETag(incidence), QStringLiteral("\"123456\"")); // Update simple case. m_agent->updateHrefETag(QStringLiteral("123456-single"), QStringLiteral("/testCal/123456-single.ics"), QStringLiteral("\"456789\"")); incidence = m_agent->mCalendar->event(QStringLiteral("123456-single")); QCOMPARE(fetchUri(incidence), QStringLiteral("/testCal/123456-single.ics")); QCOMPARE(fetchETag(incidence), QStringLiteral("\"456789\"")); // Recuring case. m_agent->updateHrefETag(QStringLiteral("123456-recurs"), QStringLiteral("/testCal/123456-recurs.ics"), QStringLiteral("\"123456\"")); incidence = m_agent->mCalendar->event(QStringLiteral("123456-recurs")); QCOMPARE(fetchUri(incidence), QStringLiteral("/testCal/123456-recurs.ics")); QCOMPARE(fetchETag(incidence), QStringLiteral("\"123456\"")); incidence = m_agent->mCalendar->event(QStringLiteral("123456-recurs"), refId); QCOMPARE(fetchUri(incidence), QStringLiteral("/testCal/123456-recurs.ics")); QCOMPARE(fetchETag(incidence), QStringLiteral("\"123456\"")); // Update recuring case. m_agent->updateHrefETag(QStringLiteral("123456-recurs"), QStringLiteral("/testCal/123456-recurs.ics"), QStringLiteral("\"456789\"")); incidence = m_agent->mCalendar->event(QStringLiteral("123456-recurs")); QCOMPARE(fetchUri(incidence), QStringLiteral("/testCal/123456-recurs.ics")); QCOMPARE(fetchETag(incidence), QStringLiteral("\"456789\"")); incidence = m_agent->mCalendar->event(QStringLiteral("123456-recurs"), refId); QCOMPARE(fetchUri(incidence), QStringLiteral("/testCal/123456-recurs.ics")); QCOMPARE(fetchETag(incidence), QStringLiteral("\"456789\"")); } static bool incidenceListContains(const KCalendarCore::Incidence::List &list, const KCalendarCore::Incidence::Ptr &ev) { for (KCalendarCore::Incidence::List::ConstIterator it = list.constBegin(); it != list.constEnd(); it++) { if ((*it)->uid() == ev->uid() && (!ev->hasRecurrenceId() || (*it)->recurrenceId() == ev->recurrenceId())) { return true; } } return false; } void tst_NotebookSyncAgent::calculateDelta() { QHash remoteUriEtags; QDateTime cur = QDateTime::currentDateTimeUtc(); // Populate the database. KCalendarCore::Incidence::Ptr ev222 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev222->setSummary("local modification"); ev222->addComment(QStringLiteral("buteo:caldav:uri:%1222.ics").arg(m_agent->mRemoteCalendarPath)); ev222->addComment(QStringLiteral("buteo:caldav:etag:\"%1\"").arg("etag222")); m_agent->mCalendar->addEvent(ev222.staticCast(), m_agent->mNotebook->uid()); KCalendarCore::Incidence::Ptr ev333 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev333->setSummary("local deletion"); ev333->addComment(QStringLiteral("buteo:caldav:uri:%1333.ics").arg(m_agent->mRemoteCalendarPath)); ev333->addComment(QStringLiteral("buteo:caldav:etag:\"%1\"").arg("etag333")); m_agent->mCalendar->addEvent(ev333.staticCast(), m_agent->mNotebook->uid()); KCalendarCore::Incidence::Ptr ev444 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev444->addComment(QStringLiteral("buteo:caldav:uri:%1444.ics").arg(m_agent->mRemoteCalendarPath)); ev444->addComment(QStringLiteral("buteo:caldav:etag:\"%1\"").arg("etag444")); ev444->setSummary("local modification discarded by a remote modification"); m_agent->mCalendar->addEvent(ev444.staticCast(), m_agent->mNotebook->uid()); KCalendarCore::Incidence::Ptr ev555 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev555->addComment(QStringLiteral("buteo:caldav:uri:%1555.ics").arg(m_agent->mRemoteCalendarPath)); ev555->addComment(QStringLiteral("buteo:caldav:etag:\"%1\"").arg("etag555")); ev555->setSummary("local modification discarded by a remote deletion"); ev555->setDtStart(cur); m_agent->mCalendar->addEvent(ev555.staticCast(), m_agent->mNotebook->uid()); KCalendarCore::Incidence::Ptr ev666 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev666->addComment(QStringLiteral("buteo:caldav:uri:%1666.ics").arg(m_agent->mRemoteCalendarPath)); ev666->addComment(QStringLiteral("buteo:caldav:etag:\"%1\"").arg("etag666")); ev666->setSummary("remote modification"); m_agent->mCalendar->addEvent(ev666.staticCast(), m_agent->mNotebook->uid()); KCalendarCore::Incidence::Ptr ev777 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev777->addComment(QStringLiteral("buteo:caldav:uri:%1777.ics").arg(m_agent->mRemoteCalendarPath)); ev777->addComment(QStringLiteral("buteo:caldav:etag:\"%1\"").arg("etag777")); ev777->setSummary("remote deletion"); ev777->setDtStart(cur.addDays(-1)); ev777.staticCast()->setDtEnd(cur.addDays(1)); m_agent->mCalendar->addEvent(ev777.staticCast(), m_agent->mNotebook->uid()); KCalendarCore::Incidence::Ptr ev888 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev888->setUid("888"); ev888->setRecurrenceId(QDateTime()); ev888->addComment(QStringLiteral("buteo:caldav:uri:%1888.ics").arg(m_agent->mRemoteCalendarPath)); ev888->addComment(QStringLiteral("buteo:caldav:etag:\"%1\"").arg("etag888")); ev888->setSummary("unchanged synced incidence"); QDateTime recId = QDateTime::currentDateTimeUtc(); recId.setTime(QTime(recId.time().hour(), recId.time().minute(), recId.time().second())); ev888->setDtStart( recId ); ev888->recurrence()->addRDateTime(recId.addDays(1)); m_agent->mCalendar->addEvent(ev888.staticCast(), m_agent->mNotebook->uid()); KCalendarCore::Incidence::Ptr ev889(ev888->clone()); ev889->setRecurrenceId(recId.addDays(1)); ev889->clearRecurrence(); ev889->setSummary("import exception to ev888"); ev889->clearComments(); // Imported exceptions don't have URI and ETAG from parent m_agent->mCalendar->addEvent(ev889.staticCast(), m_agent->mNotebook->uid()); KCalendarCore::Incidence::Ptr ev112 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev112->setSummary("partial local addition, need download"); m_agent->mCalendar->addEvent(ev112.staticCast(), m_agent->mNotebook->uid()); KCalendarCore::Incidence::Ptr ev113 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev113->setSummary("partial local modification, need upload"); m_agent->mCalendar->addEvent(ev113.staticCast(), m_agent->mNotebook->uid()); KCalendarCore::Incidence::Ptr ev001 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev001->setSummary("shared event, out-side sync window"); ev001->setDtStart(cur.addDays(-7)); ev001->addComment(QStringLiteral("buteo:caldav:uri:%1001.ics").arg(m_agent->mRemoteCalendarPath)); ev001->addComment(QStringLiteral("buteo:caldav:etag:\"%1\"").arg("etag001")); m_agent->mCalendar->addEvent(ev001.staticCast(), m_agent->mNotebook->uid()); m_agent->mStorage->save(); QDateTime lastSync = QDateTime::currentDateTimeUtc(); m_agent->mNotebook->setSyncDate(lastSync.addSecs(1)); // Sleep a bit to ensure that modification done after the sleep will have // dates that are later than creation ones, so inquiring the local database // with modifications strictly later than lastSync value will actually // returned the right events and not all. QThread::sleep(3); // Perform local modifications. KCalendarCore::Incidence::Ptr ev111 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev111->setSummary("local addition"); m_agent->mCalendar->addEvent(ev111.staticCast(), m_agent->mNotebook->uid()); ev113->setDescription(QStringLiteral("Modified summary.")); ev222->setDescription(QStringLiteral("Modified summary.")); m_agent->mCalendar->deleteIncidence(ev333); ev444->setDescription(QStringLiteral("Modified summary.")); ev555->setDescription(QStringLiteral("Modified summary.")); KCalendarCore::Incidence::Ptr ev999 = m_agent->mCalendar->dissociateSingleOccurrence(ev888, recId); QVERIFY(ev999); ev999->setSummary("local addition of persistent exception"); m_agent->mCalendar->addEvent(ev999.staticCast(), m_agent->mNotebook->uid()); m_agent->mStorage->save(); // Generate server etag reply. remoteUriEtags.insert(QStringLiteral("%1000.ics").arg(m_agent->mRemoteCalendarPath), QStringLiteral("\"etag000\"")); remoteUriEtags.insert(QStringLiteral("%1%2.ics").arg(m_agent->mRemoteCalendarPath).arg(ev112->uid()), QStringLiteral("\"etag112\"")); remoteUriEtags.insert(QStringLiteral("%1%2.ics").arg(m_agent->mRemoteCalendarPath).arg(ev113->uid()), QStringLiteral("\"etag113\"")); remoteUriEtags.insert(QStringLiteral("%1222.ics").arg(m_agent->mRemoteCalendarPath), QStringLiteral("\"etag222\"")); remoteUriEtags.insert(QStringLiteral("%1333.ics").arg(m_agent->mRemoteCalendarPath), QStringLiteral("\"etag333\"")); remoteUriEtags.insert(QStringLiteral("%1444.ics").arg(m_agent->mRemoteCalendarPath), QStringLiteral("\"etag444-1\"")); remoteUriEtags.insert(QStringLiteral("%1666.ics").arg(m_agent->mRemoteCalendarPath), QStringLiteral("\"etag666-1\"")); remoteUriEtags.insert(QStringLiteral("%1888.ics").arg(m_agent->mRemoteCalendarPath), QStringLiteral("\"etag888\"")); // Create the sync window by hand. m_agent->mFromDateTime = cur.addSecs(-1); m_agent->mToDateTime = cur.addSecs(30); QVERIFY(m_agent->calculateDelta(remoteUriEtags, &m_agent->mLocalAdditions, &m_agent->mLocalModifications, &m_agent->mLocalDeletions, &m_agent->mRemoteChanges, &m_agent->mRemoteDeletions)); QCOMPARE(m_agent->mLocalAdditions.count(), 3); QVERIFY(incidenceListContains(m_agent->mLocalAdditions, ev111)); QVERIFY(incidenceListContains(m_agent->mLocalAdditions, ev999)); QVERIFY(incidenceListContains(m_agent->mLocalAdditions, ev889)); QCOMPARE(m_agent->mLocalModifications.count(), 2); QVERIFY(incidenceListContains(m_agent->mLocalModifications, ev222)); QVERIFY(incidenceListContains(m_agent->mLocalModifications, ev113)); // ev444 have been locally modified, but is not in mLocalModifications // because of precedence of remote modifications by default. QCOMPARE(m_agent->mLocalDeletions.count(), 1); QCOMPARE(m_agent->mLocalDeletions.first()->uid(), ev333->uid()); QCOMPARE(m_agent->mRemoteChanges.count(), 4); QVERIFY(m_agent->mRemoteChanges.contains (QStringLiteral("%1000.ics").arg(m_agent->mRemoteCalendarPath))); QVERIFY(m_agent->mRemoteChanges.contains (QStringLiteral("%1%2.ics").arg(m_agent->mRemoteCalendarPath).arg(ev112->uid()))); QVERIFY(m_agent->mRemoteChanges.contains (QStringLiteral("%1444.ics").arg(m_agent->mRemoteCalendarPath))); QVERIFY(m_agent->mRemoteChanges.contains (QStringLiteral("%1666.ics").arg(m_agent->mRemoteCalendarPath))); uint nFound = 0, nNotFound = 0; Q_FOREACH(const KCalendarCore::Incidence::Ptr &incidence, m_agent->mRemoteDeletions) { if (incidence->uid() == ev555->uid() || incidence->uid() == ev777->uid()) { nFound += 1; } if (incidence->uid() == ev001->uid()) { nNotFound += 1; } } QCOMPARE(nFound, uint(2)); QCOMPARE(nNotFound, uint(0)); } Q_DECLARE_METATYPE(KCalendarCore::Incidence::Ptr) void tst_NotebookSyncAgent::oneDownSyncCycle_data() { QTest::addColumn("notebookId"); QTest::addColumn("uid"); QTest::addColumn("events"); KCalendarCore::Incidence::Ptr ev; ev = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev->setSummary("Simple event"); ev->setCreated(QDateTime::currentDateTimeUtc().addDays(-1)); QTest::newRow("simple event") << QStringLiteral("notebook-down-1") << QStringLiteral("111") << (KCalendarCore::Incidence::List() << ev); ev = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev->setSummary("Recurent event"); ev->setDtStart(QDateTime::currentDateTimeUtc()); ev->recurrence()->setDaily(1); ev->recurrence()->setDuration(28); QDateTime refId = ev->recurrence()->getNextDateTime(ev->dtStart().addDays(4)); KCalendarCore::Incidence::Ptr ex = KCalendarCore::Calendar::createException(ev, refId); QVERIFY(ex); ex->setSummary("Persistent exception"); QTest::newRow("recurent event with exception") << QStringLiteral("notebook-down-2") << QStringLiteral("222") << (KCalendarCore::Incidence::List() << ev << ex); refId = ev->recurrence()->getNextDateTime(ev->dtStart().addDays(2)); ex = KCalendarCore::Calendar::createException(ev, refId); QVERIFY(ex); ex->setSummary("orphan event"); QTest::newRow("orphan persistent exception event") << QStringLiteral("notebook-down-3") << QStringLiteral("333") << (KCalendarCore::Incidence::List() << ex); ex->setSummary("modified persistent exception event"); QTest::newRow("modified persistent exception event") << QStringLiteral("notebook-down-3") << QStringLiteral("333") << (KCalendarCore::Incidence::List() << ex); } void tst_NotebookSyncAgent::oneDownSyncCycle() { QFETCH(QString, notebookId); QFETCH(QString, uid); QFETCH(KCalendarCore::Incidence::List, events); QHash remoteUriEtags; /* We read or create a notebook for this test. */ mKCal::Notebook::Ptr notebook = m_agent->mStorage->notebook(notebookId); if (notebook) { QVERIFY(m_agent->mStorage->loadNotebookIncidences(notebook->uid())); } else { notebook = mKCal::Notebook::Ptr(new mKCal::Notebook(notebookId, "test1", "test 1", "red", true, false, false, false, false)); m_agent->mStorage->addNotebook(notebook); } m_agent->mNotebook = notebook; KCalendarCore::MemoryCalendar::Ptr memoryCalendar(new KCalendarCore::MemoryCalendar(QTimeZone::utc())); for (KCalendarCore::Incidence::List::Iterator it = events.begin(); it != events.end(); it++) { (*it)->setUid(uid); if ((*it)->recurs()) { Q_FOREACH (KCalendarCore::Incidence::Ptr instance, events) { KCalendarCore::DateTimeList exDateTimes = (*it)->recurrence()->exDateTimes(); exDateTimes.removeAll(instance->recurrenceId()); (*it)->recurrence()->setExDateTimes(exDateTimes); } } memoryCalendar->addIncidence(*it); } // The sync date is the one at the start of the process. Due to network // latencies, the actual updateIncidences() call may happen some seconds // after the stored sync date. We simulate this here with a -2 time shift. m_agent->mNotebookSyncedDateTime = QDateTime::currentDateTimeUtc().addSecs(-2); KCalendarCore::ICalFormat icalFormat; QString uri(QStringLiteral("/testCal/%1.ics").arg(uid)); QString etag(QStringLiteral("\"etag-%1\"").arg(uid)); QString response("\n" "\n"); response += QString(" \n" " %1\n" " \n" " \n" " %2\n").arg(uri).arg(etag); response += QString(" \n"); response += icalFormat.toString(memoryCalendar, QString(), false); response += QString(" \n"); response += QString(" \n" " HTTP/1.1 200 OK\n" " \n" " \n" "\n"); remoteUriEtags.insert(uri, etag); // Populate the database with the initial import, like in a slow sync. Reader reader; reader.read(response.toUtf8()); QVERIFY(!reader.hasError()); QCOMPARE(reader.results().count(), 1); QCOMPARE(reader.results()[0].incidences.count(), events.count()); QVERIFY(m_agent->updateIncidences(QList() << reader.results())); m_agent->mStorage->save(); m_agent->mNotebook->setSyncDate(m_agent->mNotebookSyncedDateTime); // Compute delta and check that nothing has changed indeed. QVERIFY(m_agent->calculateDelta(remoteUriEtags, &m_agent->mLocalAdditions, &m_agent->mLocalModifications, &m_agent->mLocalDeletions, &m_agent->mRemoteChanges, &m_agent->mRemoteDeletions)); QCOMPARE(m_agent->mLocalAdditions.count(), 0); QCOMPARE(m_agent->mLocalModifications.count(), 0); QCOMPARE(m_agent->mLocalDeletions.count(), 0); QCOMPARE(m_agent->mRemoteChanges.count(), 0); QCOMPARE(m_agent->mRemoteDeletions.count(), 0); } void tst_NotebookSyncAgent::oneUpSyncCycle_data() { QTest::addColumn("uid"); QTest::addColumn("events"); KCalendarCore::Incidence::Ptr ev; ev = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev->setSummary("Simple added event"); QTest::newRow("simple added event") << QStringLiteral("100") << (KCalendarCore::Incidence::List() << ev); ev = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev->setSummary("Recurent event"); ev->setDtStart(QDateTime::currentDateTimeUtc()); ev->recurrence()->setDaily(1); ev->recurrence()->setDuration(28); QDateTime refId = ev->recurrence()->getNextDateTime(ev->dtStart().addDays(4)); KCalendarCore::Incidence::Ptr ex = KCalendarCore::Calendar::createException(ev, refId); QVERIFY(ex); ex->setSummary("Persistent exception"); QTest::newRow("added recurent event with exception") << QStringLiteral("200") << (KCalendarCore::Incidence::List() << ev << ex); } void tst_NotebookSyncAgent::oneUpSyncCycle() { QFETCH(QString, uid); QFETCH(KCalendarCore::Incidence::List, events); QHash remoteUriEtags; static int id = 0; /* We create a notebook for this test. */ const QString nbook = QStringLiteral("notebook-up-%1").arg(id++); mKCal::Notebook::Ptr notebook = m_agent->mStorage->notebook(nbook); if (!notebook) { notebook = mKCal::Notebook::Ptr(new mKCal::Notebook(nbook, "test1", "test 1", "red", true, false, false, false, false)); m_agent->mStorage->addNotebook(notebook); } m_agent->mNotebook = notebook; const QString nbuid = QStringLiteral("NBUID:%1:%2").arg(nbook).arg(uid); const QString uri = QStringLiteral("/testCal/%1.ics").arg(nbuid); const QString etag = QStringLiteral("\"etag-%1\"").arg(uid); for (KCalendarCore::Incidence::List::Iterator it = events.begin(); it != events.end(); it++) { (*it)->setUid(nbuid); QVERIFY(m_agent->mCalendar->addEvent(it->staticCast(), m_agent->mNotebook->uid())); } m_agent->mStorage->save(); m_agent->mNotebook->setSyncDate(QDateTime::currentDateTimeUtc()); // Compute delta and check that nothing has changed indeed. QVERIFY(m_agent->calculateDelta(remoteUriEtags, &m_agent->mLocalAdditions, &m_agent->mLocalModifications, &m_agent->mLocalDeletions, &m_agent->mRemoteChanges, &m_agent->mRemoteDeletions)); QCOMPARE(m_agent->mLocalAdditions.count(), events.count()); QCOMPARE(m_agent->mLocalModifications.count(), 0); QCOMPARE(m_agent->mLocalDeletions.count(), 0); QCOMPARE(m_agent->mRemoteChanges.count(), 0); QCOMPARE(m_agent->mRemoteDeletions.count(), 0); // Simulate reception of etags for each event. remoteUriEtags.insert(uri, etag); m_agent->updateHrefETag(nbuid, uri, etag); m_agent->mStorage->save(); m_agent->mNotebook->setSyncDate(QDateTime::currentDateTimeUtc()); // TODO: move these clear statements inside delta ? m_agent->mLocalAdditions.clear(); m_agent->mLocalModifications.clear(); m_agent->mLocalDeletions.clear(); m_agent->mRemoteChanges.clear(); m_agent->mRemoteDeletions.clear(); // Compute delta again and check that nothing has changed indeed. QVERIFY(m_agent->calculateDelta(remoteUriEtags, &m_agent->mLocalAdditions, &m_agent->mLocalModifications, &m_agent->mLocalDeletions, &m_agent->mRemoteChanges, &m_agent->mRemoteDeletions)); QCOMPARE(m_agent->mLocalAdditions.count(), 0); QCOMPARE(m_agent->mLocalModifications.count(), 0); QCOMPARE(m_agent->mLocalDeletions.count(), 0); QCOMPARE(m_agent->mRemoteChanges.count(), 0); QCOMPARE(m_agent->mRemoteDeletions.count(), 0); } void tst_NotebookSyncAgent::updateIncidence_data() { QTest::addColumn("incidence"); { KCalendarCore::Incidence::Ptr ev = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev->setUid("updateIncidence-111"); ev->setSummary("Simple added event"); QTest::newRow("simple added event") << ev; } { KCalendarCore::Incidence::Ptr ev = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev->setUid("updateIncidence-111"); ev->setSummary("Simple updated event"); QTest::newRow("simple updated event") << ev; } { KCalendarCore::Incidence::Ptr ev = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev->setUid("updateIncidence-222"); ev->setSummary("Recurring added event"); ev->setDtStart(QDateTime::currentDateTimeUtc()); ev->recurrence()->setDaily(1); QTest::newRow("Recurring added event") << ev; KCalendarCore::Incidence::Ptr ex = KCalendarCore::Incidence::Ptr(ev->clone()); ex->setSummary("Added exception"); ex->setRecurrenceId(ev->dtStart().addDays(1)); ex->clearRecurrence(); QTest::newRow("Added exception") << ex; } { KCalendarCore::Incidence::Ptr ev = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev->setUid("updateIncidence-333"); ev->setSummary("Recurring added orphan"); ev->setDtStart(QDateTime::currentDateTimeUtc()); ev->setRecurrenceId(ev->dtStart().addDays(1)); QTest::newRow("Recurring added event") << ev; } { KCalendarCore::Incidence::Ptr ev = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); ev->setUid("updateIncidence-444"); ev->setSummary("Recurring added event with an EXDATE"); ev->setDtStart(QDateTime::currentDateTimeUtc()); ev->recurrence()->setDaily(1); ev->recurrence()->addExDateTime(ev->dtStart().addDays(1)); QTest::newRow("Recurring added event with an EXDATE") << ev; // Normally, one should not add an exception at an exdate, // but some faulty servers are sending recurring events with exdates // and exceptions during these exdates. Since, the exception // is actually known, it's better for the user to relax // this case and add the exception anyway. KCalendarCore::Incidence::Ptr ex = KCalendarCore::Incidence::Ptr(ev->clone()); ex->setSummary("Added exception at a faulty EXDATE"); ex->setRecurrenceId(ev->dtStart().addDays(1)); ex->clearRecurrence(); QTest::newRow("Added exception at a faulty EXDATE") << ex; } } void tst_NotebookSyncAgent::updateIncidence() { QFETCH(KCalendarCore::Incidence::Ptr, incidence); /* We create a notebook for this test. */ const QString notebookId = QStringLiteral("26b24ae3-ab05-4892-ac36-632183113e2d"); mKCal::Notebook::Ptr notebook = m_agent->mStorage->notebook(notebookId); if (!notebook) { notebook = mKCal::Notebook::Ptr(new mKCal::Notebook(notebookId, "test1", "test 1", "red", true, false, false, false, false)); m_agent->mStorage->addNotebook(notebook); } m_agent->mNotebook = notebook; Reader::CalendarResource resource; resource.href = QStringLiteral("uri.ics"); resource.etag = QStringLiteral("etag"); resource.incidences << incidence; QVERIFY(m_agent->updateIncidences(QList() << resource)); KCalendarCore::Incidence::Ptr fetched = m_agent->mCalendar->incidence(incidence->uid(), incidence->recurrenceId()); // Created date may differ on dissociated occurrences, artificially set it. incidence->setCreated(fetched->created()); // Fetched will have an added end date because of dissociateSingleOccurrence() if (fetched->type() == KCalendarCore::Incidence::TypeEvent && fetched.staticCast()->hasEndDate()) { incidence.staticCast()->setDtEnd(fetched.staticCast()->dtEnd()); } QCOMPARE(*incidence, *fetched); } void tst_NotebookSyncAgent::requestFinished() { QSignalSpy finished(m_agent, &NotebookSyncAgent::finished); Report *report = new Report(m_agent->mNetworkManager, m_agent->mSettings); m_agent->mRequests.insert(report); QCOMPARE(m_agent->mRequests.count(), 1); m_agent->requestFinished(report); QCOMPARE(m_agent->mRequests.count(), 0); QCOMPARE(finished.count(), 1); finished.clear(); Put *put1 = new Put(m_agent->mNetworkManager, m_agent->mSettings); m_agent->mRequests.insert(put1); QCOMPARE(m_agent->mRequests.count(), 1); Put *put2 = new Put(m_agent->mNetworkManager, m_agent->mSettings); m_agent->mRequests.insert(put2); QCOMPARE(m_agent->mRequests.count(), 2); m_agent->requestFinished(put2); QCOMPARE(m_agent->mRequests.count(), 1); QCOMPARE(finished.count(), 0); QVERIFY(m_agent->mRequests.contains(put1)); m_agent->requestFinished(put1); QCOMPARE(m_agent->mRequests.count(), 0); QCOMPARE(finished.count(), 1); } void tst_NotebookSyncAgent::result() { m_agent->mSyncMode = NotebookSyncAgent::QuickSync; KCalendarCore::Incidence::Ptr rAdd1 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); rAdd1->addComment(QStringLiteral("buteo:caldav:uri:/path/event1")); KCalendarCore::Incidence::Ptr rAdd2 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); rAdd2->addComment(QStringLiteral("buteo:caldav:uri:/path/event2")); KCalendarCore::Incidence::Ptr rAdd3 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); rAdd3->addComment(QStringLiteral("buteo:caldav:uri:/path/event3")); m_agent->mRemoteAdditions = KCalendarCore::Incidence::List() << rAdd1 << rAdd2 << rAdd3; KCalendarCore::Incidence::Ptr rDel1 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); rDel1->addComment(QStringLiteral("buteo:caldav:uri:/path/event01")); KCalendarCore::Incidence::Ptr rDel2 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); rDel2->addComment(QStringLiteral("buteo:caldav:uri:/path/event02")); KCalendarCore::Incidence::Ptr rDel3 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); rDel3->addComment(QStringLiteral("buteo:caldav:uri:/path/event03")); KCalendarCore::Incidence::Ptr rDel4 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); rDel4->addComment(QStringLiteral("buteo:caldav:uri:/path/event04")); m_agent->mRemoteDeletions = KCalendarCore::Incidence::List() << rDel1 << rDel2 << rDel3 << rDel4; KCalendarCore::Incidence::Ptr rMod1 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); rMod1->addComment(QStringLiteral("buteo:caldav:uri:/path/event11")); KCalendarCore::Incidence::Ptr rMod2 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); rMod2->addComment(QStringLiteral("buteo:caldav:uri:/path/event12")); KCalendarCore::Incidence::Ptr rMod3 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); rMod3->addComment(QStringLiteral("buteo:caldav:uri:/path/event13")); KCalendarCore::Incidence::Ptr rMod4 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); rMod4->addComment(QStringLiteral("buteo:caldav:uri:/path/event14")); KCalendarCore::Incidence::Ptr rMod5 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); rMod5->addComment(QStringLiteral("buteo:caldav:uri:/path/event15")); m_agent->mRemoteModifications = KCalendarCore::Incidence::List() << rMod1 << rMod2 << rMod3 << rMod4 << rMod5; KCalendarCore::Incidence::Ptr lAdd1 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); lAdd1->setUid("event001"); KCalendarCore::Incidence::Ptr lAdd2 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); lAdd2->setUid("event002"); m_agent->mLocalAdditions = KCalendarCore::Incidence::List() << lAdd1 << lAdd2; KCalendarCore::Incidence::Ptr lDel1 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); lDel1->addComment(QStringLiteral("buteo:caldav:uri:/path/event101")); KCalendarCore::Incidence::Ptr lDel2 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); lDel2->addComment(QStringLiteral("buteo:caldav:uri:/path/event102")); KCalendarCore::Incidence::Ptr lDel3 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); lDel3->addComment(QStringLiteral("buteo:caldav:uri:/path/event103")); m_agent->mLocalDeletions = KCalendarCore::Incidence::List() << lDel1 << lDel2 << lDel3; KCalendarCore::Incidence::Ptr lMod1 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); lMod1->addComment(QStringLiteral("buteo:caldav:uri:/path/event111")); KCalendarCore::Incidence::Ptr lMod2 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); lMod2->addComment(QStringLiteral("buteo:caldav:uri:/path/event112")); KCalendarCore::Incidence::Ptr lMod3 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); lMod3->addComment(QStringLiteral("buteo:caldav:uri:/path/event113")); KCalendarCore::Incidence::Ptr lMod4 = KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); lMod4->addComment(QStringLiteral("buteo:caldav:uri:/path/event114")); m_agent->mLocalModifications = KCalendarCore::Incidence::List() << lMod1 << lMod2 << lMod3 << lMod4; m_agent->mFailingUpdates.insert("/path/event1", QByteArrayLiteral("test1")); m_agent->mFailingUpdates.insert("/path/event01", QByteArrayLiteral("test1")); m_agent->mFailingUpdates.insert("/path/event11", QByteArrayLiteral("test1")); m_agent->mFailingUploads.insert("/testCal/event001.ics", QByteArrayLiteral("test1")); m_agent->mFailingUploads.insert("/path/event101", QByteArrayLiteral("test1")); m_agent->mFailingUploads.insert("/path/event111", QByteArrayLiteral("test1")); Buteo::TargetResults results = m_agent->result(); QCOMPARE(results.targetName(), QLatin1String("test1")); QCOMPARE(results.remoteItems().added, unsigned(1)); QCOMPARE(results.remoteItems().deleted, unsigned(2)); QCOMPARE(results.remoteItems().modified, unsigned(3)); QCOMPARE(results.localItems().added, unsigned(2)); QCOMPARE(results.localItems().deleted, unsigned(3)); QCOMPARE(results.localItems().modified, unsigned(4)); m_agent->mSyncMode = NotebookSyncAgent::SlowSync; Reader::CalendarResource r1; r1.href = "/path/event1"; r1.incidences << KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); Reader::CalendarResource r2; r2.href = "/path/event2"; r2.incidences << KCalendarCore::Incidence::Ptr(new KCalendarCore::Event); m_agent->mReceivedCalendarResources = QList() << r1 << r2; results = m_agent->result(); QCOMPARE(results.targetName(), QLatin1String("test1")); QCOMPARE(results.remoteItems().added, unsigned(0)); QCOMPARE(results.remoteItems().deleted, unsigned(0)); QCOMPARE(results.remoteItems().modified, unsigned(0)); QCOMPARE(results.localItems().added, unsigned(1)); QCOMPARE(results.localItems().deleted, unsigned(0)); QCOMPARE(results.localItems().modified, unsigned(0)); } #include "tst_notebooksyncagent.moc" QTEST_MAIN(tst_NotebookSyncAgent) buteo-sync-plugin-caldav-0.3.14/tests/propfind/000077500000000000000000000000001467717066200214165ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/tests/propfind/propfind.pro000066400000000000000000000003221467717066200237560ustar00rootroot00000000000000TEMPLATE = app TARGET = tst_propfind QT += testlib QT -= gui CONFIG += debug include($$PWD/../../src/src.pri) SOURCES += tst_propfind.cpp target.path = /opt/tests/buteo/plugins/caldav/ INSTALLS += target buteo-sync-plugin-caldav-0.3.14/tests/propfind/tst_propfind.cpp000066400000000000000000000412741467717066200246450ustar00rootroot00000000000000/* -*- c-basic-offset: 4 -*- */ /* * Copyright (C) 2020 Caliste Damien. * Contact: Damien Caliste * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include class tst_Propfind : public QObject { Q_OBJECT public: tst_Propfind(); virtual ~tst_Propfind(); public slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); private slots: void parseUserPrincipalResponse_data(); void parseUserPrincipalResponse(); void parseUserAddressSetResponse_data(); void parseUserAddressSetResponse(); void parseCalendarResponse_data(); void parseCalendarResponse(); private: QNetworkAccessManager *mNAManager; Settings mSettings; PropFind *mRequest; }; tst_Propfind::tst_Propfind() { } tst_Propfind::~tst_Propfind() { } void tst_Propfind::initTestCase() { mNAManager = new QNetworkAccessManager; } void tst_Propfind::cleanupTestCase() { delete mNAManager; } void tst_Propfind::init() { mRequest = new PropFind(mNAManager, &mSettings); } void tst_Propfind::cleanup() { delete mRequest; } void tst_Propfind::parseUserPrincipalResponse_data() { QTest::addColumn("data"); QTest::addColumn("success"); QTest::addColumn("userPrincipal"); QTest::newRow("empty response") << QByteArray() << false << QString(); QTest::newRow("invalid response") << QByteArray("/") << false << QString(); QTest::newRow("forbidden access") << QByteArray("/HTTP/1.1 403") << false << QString(); QTest::newRow("valid response") << QByteArray("//principals/users/username%40server.tld/HTTP/1.1 200 OK") << true << QString::fromLatin1("/principals/users/username%40server.tld/"); } void tst_Propfind::parseUserPrincipalResponse() { QFETCH(QByteArray, data); QFETCH(bool, success); QFETCH(QString, userPrincipal); QCOMPARE(mRequest->parseUserPrincipalResponse(data), success); QCOMPARE(mRequest->userPrincipal(), userPrincipal); } void tst_Propfind::parseUserAddressSetResponse_data() { QTest::addColumn("data"); QTest::addColumn("success"); QTest::addColumn("userMailtoHref"); QTest::addColumn("userHomeHref"); QTest::newRow("empty response") << QByteArray() << false << QString() << QString(); QTest::newRow("invalid response") << QByteArray("/principals/users/username%40server.tld/") << false << QString() << QString(); QTest::newRow("forbidden access") << QByteArray("/principals/users/username%40server.tld/HTTP/1.1 403") << false << QString() << QString(); QTest::newRow("valid mailto") << QByteArray("/principals/users/username%40server.tld/mailto:username@server.tld/principals/users/username%40server.tld/HTTP/1.1 200 OK") << true << QString::fromLatin1("username@server.tld") << QString(); QTest::newRow("valid home") << QByteArray("/principals/users/username%40server.tld//caldav/HTTP/1.1 200 OKHTTP/1.1 404") << true << QString() << QString("/caldav/"); } void tst_Propfind::parseUserAddressSetResponse() { QFETCH(QByteArray, data); QFETCH(bool, success); QFETCH(QString, userMailtoHref); QCOMPARE(mRequest->parseUserAddressSetResponse(data), success); QCOMPARE(mRequest->userMailtoHref(), userMailtoHref); } Q_DECLARE_METATYPE(PropFind::CalendarInfo) void tst_Propfind::parseCalendarResponse_data() { QTest::addColumn("data"); QTest::addColumn("success"); QTest::addColumn>("calendars"); QTest::newRow("empty response") << QByteArray() << false << QList(); QTest::newRow("invalid response") << QByteArray("/calendars/0/") << false << QList(); QTest::newRow("forbidden access") << QByteArray("/calendars/0/HTTP/1.1 403") << true << QList(); QTest::newRow("not a calendar") << QByteArray("/calendars//principals/users/username%40server.tld/HTTP/1.1 200 OK") << true << QList(); QTest::newRow("one valid calendar") << QByteArray("/calendars/0/Calendar 0#FF0000/principals/users/username%40server.tld/HTTP/1.1 200 OK") << true << (QList() << PropFind::CalendarInfo{ QString::fromLatin1("/calendars/0/"), QString::fromLatin1("Calendar 0"), QString::fromLatin1("#FF0000"), QString::fromLatin1("/principals/users/username%40server.tld/")}); QTest::newRow("one read-only calendar") << QByteArray("/calendars/0/Calendar 0#FF0000/principals/users/username%40server.tld/HTTP/1.1 200 OK") << true << (QList() << PropFind::CalendarInfo{ QString::fromLatin1("/calendars/0/"), QString::fromLatin1("Calendar 0"), QString::fromLatin1("#FF0000"), QString::fromLatin1("/principals/users/username%40server.tld/"), true}); QTest::newRow("missing current-user-principal") << QByteArray("/calendars/0/Calendar 0#FF0000HTTP/1.1 200 OKHTTP/1.1 404") << true << (QList() << PropFind::CalendarInfo{ QString::fromLatin1("/calendars/0/"), QString::fromLatin1("Calendar 0"), QString::fromLatin1("#FF0000"), QString()}); QTest::newRow("missing displayname") << QByteArray("/calendars/0/#FF0000/principals/users/username%40server.tld/HTTP/1.1 200 OKHTTP/1.1 404") << true << (QList() << PropFind::CalendarInfo{ QString::fromLatin1("/calendars/0/"), QString::fromLatin1("Calendar"), QString::fromLatin1("#FF0000"), QString::fromLatin1("/principals/users/username%40server.tld/")}); QTest::newRow("missing privileges") << QByteArray("/calendars/0/Calendar 0#FF0000/principals/users/username%40server.tld/HTTP/1.1 200 OKHTTP/1.1 404") << true << (QList() << PropFind::CalendarInfo{ QString::fromLatin1("/calendars/0/"), QString::fromLatin1("Calendar 0"), QString::fromLatin1("#FF0000"), QString::fromLatin1("/principals/users/username%40server.tld/")}); QTest::newRow("two valid calendars") << QByteArray("/calendars/0/Calendar 0#FF0000/principals/users/username%40server.tld/HTTP/1.1 200 OK/calendars/1/Calendar 1#FFFF00/principals/users/username%40server.tld/HTTP/1.1 200 OK") << true << (QList() << PropFind::CalendarInfo{ QString::fromLatin1("/calendars/0/"), QString::fromLatin1("Calendar 0"), QString::fromLatin1("#FF0000"), QString::fromLatin1("/principals/users/username%40server.tld/")} << PropFind::CalendarInfo{ QString::fromLatin1("/calendars/1/"), QString::fromLatin1("Calendar 1"), QString::fromLatin1("#FFFF00"), QString::fromLatin1("/principals/users/username%40server.tld/")}); PropFind::CalendarInfo todos(QString::fromLatin1("/calendars/0/"), QString::fromLatin1("Calendar 0"), QString::fromLatin1("#FF0000"), QString::fromLatin1("/principals/users/username%40server.tld/")); todos.allowEvents = false; todos.allowTodos = true; todos.allowJournals = false; QTest::newRow("one valid task manager") << QByteArray("/calendars/0/Calendar 0#FF0000/principals/users/username%40server.tld/HTTP/1.1 200 OK") << true << (QList() << todos); QTest::newRow("missing component set") << QByteArray("/calendars/0/#FF0000/principals/users/username%40server.tld/HTTP/1.1 200 OKHTTP/1.1 404") << true << (QList() << PropFind::CalendarInfo{ QString::fromLatin1("/calendars/0/"), QString::fromLatin1("Calendar"), QString::fromLatin1("#FF0000"), QString::fromLatin1("/principals/users/username%40server.tld/")}); } void tst_Propfind::parseCalendarResponse() { QFETCH(QByteArray, data); QFETCH(bool, success); QFETCH(QList, calendars); QCOMPARE(mRequest->parseCalendarResponse(data), success); const QList response = mRequest->calendars(); QCOMPARE(response, calendars); } #include "tst_propfind.moc" QTEST_MAIN(tst_Propfind) buteo-sync-plugin-caldav-0.3.14/tests/reader/000077500000000000000000000000001467717066200210375ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/tests/reader/data/000077500000000000000000000000001467717066200217505ustar00rootroot00000000000000buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_CR_description.xml000066400000000000000000000012301467717066200267170ustar00rootroot00000000000000 /user/cal.ics/ BEGIN:VCALENDAR PRODID:-//ownCloud calendar v1.2.2 VERSION:2.0 BEGIN:VEVENT DTSTART:20160626T190000Z DTEND:20160626T210000Z DTSTAMP:20160606T193516Z UID:123-456@789%369$258*147 CREATED:20151216T030251Z DESCRIPTION:description\nmultilines\n LAST-MODIFIED:20160531T063923Z LOCATION:Toulouse SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Sieger F - Zweiter E TRANSP:OPAQUE END:VEVENT END:VCALENDAR buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_UID.xml000066400000000000000000000012131467717066200244320ustar00rootroot00000000000000 /user/cal.ics/ BEGIN:VCALENDAR PRODID:-//ownCloud calendar v1.2.2 VERSION:2.0 BEGIN:VEVENT DTSTART:20160626T190000Z DTEND:20160626T210000Z DTSTAMP:20160606T193516Z UID:123-456@789%369$258*147 CREATED:20151216T030251Z DESCRIPTION:description LAST-MODIFIED:20160531T063923Z LOCATION:Toulouse SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Sieger F - Zweiter E TRANSP:OPAQUE END:VEVENT END:VCALENDAR buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_UTF8_description.xml000066400000000000000000000012071467717066200271450ustar00rootroot00000000000000 /user/cal.ics/ BEGIN:VCALENDAR PRODID:-//ownCloud calendar v1.2.2 VERSION:2.0 BEGIN:VEVENT DTSTART:20160626T190000Z DTEND:20160626T210000Z DTSTAMP:20160606T193516Z UID:123456789 CREATED:20151216T030251Z DESCRIPTION:UTF8 characters: nœud LAST-MODIFIED:20160531T063923Z LOCATION:Toulouse SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Sieger F - Zweiter E TRANSP:OPAQUE END:VEVENT END:VCALENDAR buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_base.xml000066400000000000000000000014031467717066200247240ustar00rootroot00000000000000 /user/cal.ics/ BEGIN:VCALENDAR PRODID:-//Radicale//NONSGML Radicale Server//EN VERSION:2.0 BEGIN:VEVENT DTSTAMP:20160930T132609Z CREATED:20160930T132609Z X-RADICALE-NAME:972a7c13-bbd6-4fce-9ebb-03a808111828.ics UID:972a7c13-bbd6-4fce-9ebb-03a808111828 LAST-MODIFIED:20160930T132609Z SUMMARY:Test DTSTART;TZID=Europe/Paris:20160930T160000 DTEND;TZID=Europe/Paris:20160930T170000 TRANSP:OPAQUE BEGIN:VALARM ACTION: TRIGGER;VALUE=DURATION:-PT30M X-KDE-KCALCORE-ENABLED:TRUE END:VALARM END:VEVENT END:VCALENDAR buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_basic_vcal.xml000066400000000000000000000010771467717066200261070ustar00rootroot00000000000000 /user/cal.vcs/ BEGIN:VCALENDAR PRODID:-//Radicale//NONSGML Radicale Server//EN VERSION:1.0 BEGIN:VEVENT UID:572a7c13-bbd6-4fce-9ebb-03a808111828 CATEGORIES:MEETING STATUS:TENTATIVE DTSTART:19960401T033000Z DTEND:19960401T043000Z SUMMARY:Test DESCRIPTION:Test description CLASS:PRIVATE END:VEVENT END:VCALENDAR buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_cdata.xml000066400000000000000000000010071467717066200250660ustar00rootroot00000000000000 /user/cal.ics/ ef&g SUMMARY:Regarder l'hôtel en Espagne END:VEVENT END:VCALENDAR]]> buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_earlyUID.xml000066400000000000000000000010471467717066200254740ustar00rootroot00000000000000 /user/cal.ics/ BEGIN:VCALENDAR VERSION:2.0 PRODID:ownCloud Calendar 0.6.4 UID:1234567890abcdef BEGIN:VEVENT DTSTAMP:20121021T150000Z DTSTART:20161201T000000 DTEND;VALUE=DATE-TIME:20161202T000000 TRANSP:TRANSPARENT SEQUENCE:0 SUMMARY:early UID test CLASS:PUBLIC END:VEVENT END:VCALENDAR buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_fullday.xml000066400000000000000000000021241467717066200254530ustar00rootroot00000000000000 /owncloud/remote.php/dav/calendars/testcal/personal/ownCloud-qg1z0l37u7qxmct7sdrorn.ics "7d68408eb5c61bb428fe19619797f32a" BEGIN:VCALENDAR PRODID:-//ownCloud calendar v1.3.1 VERSION:2.0 BEGIN:VEVENT CREATED:20170323T195623 DTSTAMP:20170323T195623 LAST-MODIFIED:20170323T195623 UID:bgx5xrwyfd SUMMARY:All day test event 1 DTSTART;TZID=Europe/London;VALUE=DATE:20170324 DTEND;TZID=Europe/London;VALUE=DATE:20170325 END:VEVENT BEGIN:VTIMEZONE TZID:Europe/London X-LIC-LOCATION:Europe/London BEGIN:DAYLIGHT TZOFFSETFROM:+0000 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19700329T010000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0100 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:19701025T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 END:STANDARD END:VTIMEZONE END:VCALENDAR HTTP/1.1 200 OK buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_fullday_vcal.xml000066400000000000000000000021101467717066200264530ustar00rootroot00000000000000 /owncloud/remote.php/dav/calendars/testcal/personal/ownCloud-qg1z0l37u7qxmct7sdrorn.ics "7d68408eb5c61bb428fe19619797f32a" BEGIN:VCALENDAR PRODID:-//ownCloud calendar v1.3.1 BEGIN:VEVENT CREATED:20170323T195623 DTSTAMP:20170323T195623 LAST-MODIFIED:20170323T195623 UID:bgx5xrwyfd SUMMARY:All day test event 1 DTSTART;TZID=Europe/London;VALUE=DATE:20170324 DTEND;TZID=Europe/London;VALUE=DATE:20170325 END:VEVENT BEGIN:VTIMEZONE TZID:Europe/London X-LIC-LOCATION:Europe/London BEGIN:DAYLIGHT TZOFFSETFROM:+0000 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19700329T010000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0100 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:19701025T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 END:STANDARD END:VTIMEZONE END:VCALENDAR HTTP/1.1 200 OK buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_missing.xml000066400000000000000000000011611467717066200254640ustar00rootroot00000000000000 /user/cal.ics/ BEGIN:VCALENDAR PRODID:-//ownCloud calendar v1.2.2 BEGIN:VEVENT DTSTART:20160626T190000Z DTEND:20160626T210000Z DTSTAMP:20160606T193516Z UID:123456789 CREATED:20151216T030251Z DESCRIPTION:Achtelfinale LAST-MODIFIED:20160531T063923Z LOCATION:Toulouse SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Sieger F - Zweiter E TRANSP:OPAQUE END:VEVENT END:VCALENDAR buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_nodav.xml000066400000000000000000000036751467717066200251360ustar00rootroot00000000000000
ErrorException: Array to string conversion in /var/www/html/cal/vendor/sabre/dav/lib/Sabre/CalDAV/CalendarQueryValidator.php:62
Stack trace:
#0 /var/www/html/cal/vendor/sabre/dav/lib/Sabre/CalDAV/CalendarQueryValidator.php(62): Baikal\Framework::exception_error_handler(8, 'Array to string...', '/var/www/html/c...', 62, Array)
#1 /var/www/html/cal/vendor/sabre/dav/lib/Sabre/CalDAV/CalendarQueryValidator.php(41): Sabre\CalDAV\CalendarQueryValidator->validateCompFilters(Object(Sabre\VObject\Component\VCalendar), Array)
#2 /var/www/html/cal/vendor/sabre/dav/lib/Sabre/CalDAV/Backend/AbstractBackend.php(150): Sabre\CalDAV\CalendarQueryValidator->validate(Object(Sabre\VObject\Component\VCalendar), Array)
#3 /var/www/html/cal/vendor/sabre/dav/lib/Sabre/CalDAV/Backend/PDO.php(680): Sabre\CalDAV\Backend\AbstractBackend->validateFilterForObject(Array, Array)
#4 /var/www/html/cal/vendor/sabre/dav/lib/Sabre/CalDAV/Calendar.php(372): Sabre\CalDAV\Backend\PDO->calendarQuery('1', Array)
#5 /var/www/html/cal/vendor/sabre/dav/lib/Sabre/CalDAV/Plugin.php(591): Sabre\CalDAV\Calendar->calendarQuery(Array)
#6 /var/www/html/cal/vendor/sabre/dav/lib/Sabre/CalDAV/Plugin.php(265): Sabre\CalDAV\Plugin->calendarQueryReport(Object(DOMDocument))
#7 [internal function]: Sabre\CalDAV\Plugin->report('{urn:ietf:param...', Object(DOMDocument), 'calendars/kas/d...')
#8 /var/www/html/cal/vendor/sabre/dav/lib/Sabre/DAV/Server.php(433): call_user_func_array(Array, Array)
#9 /var/www/html/cal/vendor/sabre/dav/lib/Sabre/DAV/Server.php(1063): Sabre\DAV\Server->broadcastEvent('report', Array)
#10 [internal function]: Sabre\DAV\Server->httpReport('calendars/kas/d...')
#11 /var/www/html/cal/vendor/sabre/dav/lib/Sabre/DAV/Server.php(474): call_user_func(Array, 'calendars/kas/d...')
#12 /var/www/html/cal/vendor/sabre/dav/lib/Sabre/DAV/Server.php(214): Sabre\DAV\Server->invokeMethod('REPORT', 'calendars/kas/d...')
#13 /var/www/html/cal/cal.php(82): Sabre\DAV\Server->exec()
#14 {main}
buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_noevent.xml000066400000000000000000000005611467717066200254740ustar00rootroot00000000000000 /user/cal.ics/ BEGIN:VCALENDAR PRODID:-//Radicale//NONSGML Radicale Server//EN VERSION:2.0 END:VCALENDAR buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_noxml.xml000066400000000000000000000000251467717066200251460ustar00rootroot00000000000000Not a valid XML file!buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_relativealarm.xml000066400000000000000000000014421467717066200266450ustar00rootroot00000000000000 /thunderbird/1234.ics "7d68408eb5c61bb428fe19619797f32a" BEGIN:VCALENDAR PRODID:-//Thunderbird almost VERSION:2.0 BEGIN:VEVENT CREATED:20170323T195623 DTSTAMP:20170323T195623 LAST-MODIFIED:20170323T195623 UID:123456789 SUMMARY:Alarm DESCRIPTION:Alarm with relative time. DTSTART;VALUE=DATE:20170324 DTEND;VALUE=DATE:20170325 BEGIN:VALARM ACTION:DISPLAY TRIGGER;VALUE=DURATION:-PT12H DESCRIPTION:Default Mozilla Description X-KDE-KCALCORE-ENABLED:TRUE END:VALARM END:VEVENT END:VCALENDAR HTTP/1.1 200 OK buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_todo_pending.xml000066400000000000000000000013541467717066200264700ustar00rootroot00000000000000 /dcaliste/Prive.ics/20070313T123432Z-456553.ics \"78e30cfc6a537db0221a67584eea36f3\" BEGIN:VCALENDAR PRODID:-//K Desktop Environment//NONSGML libkcal 4.3//EN VERSION:2.0 X-KDE-ICAL-IMPLEMENTATION-VERSION:1.0 BEGIN:VTODO UID:20070313T123432Z-456553@example.com DTSTAMP:20070313T123432Z DUE;VALUE=DATE:20070501 SUMMARY:Submit Quebec Income Tax Return for 2006 CLASS:CONFIDENTIAL CATEGORIES:FAMILY,FINANCE STATUS:NEEDS-ACTION END:VTODO END:VCALENDAR HTTP/1.1 200 OK buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_unexpected_elements.xml000066400000000000000000000023621467717066200300570ustar00rootroot00000000000000 /calendars/p-47525/D77EB0B9-B481-44D3-A109-3D2BD7CE5A36.ics "1274112-1"text/calendarBEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VTIMEZONE TZID:Europe/Zurich BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU DTSTART:19701025T030000 END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU DTSTART:19700329T020000 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT DTSTAMP:20191015T062350Z DTSTART;TZID=Europe/Zurich:20181015T090000 DTEND;TZID=Europe/Zurich:20181015T100000 SUMMARY:Bob UID:D77EB0B9-B481-44D3-A109-3D2BD7CE5A36 RRULE:FREQ=YEARLY EXDATE;TZID=Europe/Zurich:20181015T090000 EXDATE;TZID=Europe/Zurich:20191015T090000 CREATED:20191015T062350Z LAST-MODIFIED:20191015T062350Z END:VEVENT END:VCALENDAR HTTP/1.1 200 OK buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_urldescription.xml000066400000000000000000000012561467717066200270660ustar00rootroot00000000000000 /user/cal.ics/ BEGIN:VCALENDAR PRODID:-//ownCloud calendar v1.2.2 VERSION:2.0 BEGIN:VEVENT DTSTART:20160626T190000Z DTEND:20160626T210000Z DTSTAMP:20160606T193516Z UID:123456789 CREATED:20151216T030251Z DESCRIPTION:this https://www.sailfishos.org/test?id=one&two=2 &"' ok? LAST-MODIFIED:20160531T063923Z LOCATION:Toulouse SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Sieger F - Zweiter E TRANSP:OPAQUE END:VEVENT END:VCALENDAR buteo-sync-plugin-caldav-0.3.14/tests/reader/data/reader_xmltag.xml000066400000000000000000000012271467717066200253120ustar00rootroot00000000000000 /user/cal.ics/ BEGIN:VCALENDAR PRODID:-//ownCloud calendar v1.2.2 BEGIN:VEVENT DTSTART:20160626T190000Z DTEND:20160626T210000Z DTSTAMP:20160606T193516Z UID:123456789 CREATED:20151216T030251Z DESCRIPTION:&a<b>cef&g LAST-MODIFIED:20160531T063923Z LOCATION:Toulouse SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Chrzęst gąsienic été TRANSP:OPAQUE END:VEVENT END:VCALENDAR buteo-sync-plugin-caldav-0.3.14/tests/reader/reader.pro000066400000000000000000000005071467717066200230250ustar00rootroot00000000000000TEMPLATE = app TARGET = tst_reader QT += testlib QT -= gui CONFIG += debug include($$PWD/../../src/src.pri) SOURCES += tst_reader.cpp OTHER_FILES += data/*xml datafiles.files += data/*xml datafiles.path = /opt/tests/buteo/plugins/caldav/data/ target.path = /opt/tests/buteo/plugins/caldav/ INSTALLS += target datafiles buteo-sync-plugin-caldav-0.3.14/tests/reader/tst_reader.cpp000066400000000000000000000257771467717066200237210ustar00rootroot00000000000000/* -*- c-basic-offset: 4 -*- */ /* * Copyright (C) 2016 Caliste Damien. * Contact: Damien Caliste * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include #include class tst_Reader : public QObject { Q_OBJECT public: tst_Reader(); virtual ~tst_Reader(); public slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); private slots: void readICal_data(); void readICal(); void readDate_data(); void readDate(); void readAlarm_data(); void readAlarm(); }; tst_Reader::tst_Reader() { } tst_Reader::~tst_Reader() { } void tst_Reader::initTestCase() { } void tst_Reader::cleanupTestCase() { } void tst_Reader::init() { } void tst_Reader::cleanup() { } void tst_Reader::readICal_data() { QTest::addColumn("xmlFilename"); QTest::addColumn("expectedNoError"); QTest::addColumn("expectedNResponses"); QTest::addColumn("expectedNIncidences"); QTest::addColumn("expectedUID"); QTest::addColumn("expectedSummary"); QTest::addColumn("expectedDescription"); QTest::addColumn("expectedRecurs"); QTest::addColumn("expectedNAlarms"); QTest::newRow("no XML stream") << QStringLiteral("data/reader_noxml.xml") << false << 0 << 0 << QString() << QString() << QString() << false << 0; QTest::newRow("malformed XML stream") << QStringLiteral("data/reader_nodav.xml") << false << 0 << 0 << QString() << QString() << QString() << false << 0; QTest::newRow("unexpected prop elements in response") << QStringLiteral("data/reader_unexpected_elements.xml") << true << 1 << 1 << QString("D77EB0B9-B481-44D3-A109-3D2BD7CE5A36") << QString("Bob") << QString() << true << 0; QTest::newRow("no incidence response") << QStringLiteral("data/reader_noevent.xml") << true << 1 << 0 << QString() << QString() << QString() << false << 0; QTest::newRow("basic one incidence response") << QStringLiteral("data/reader_base.xml") << true << 1 << 1 << QStringLiteral("972a7c13-bbd6-4fce-9ebb-03a808111828") << QStringLiteral("Test") << QStringLiteral("") << false << 1; QTest::newRow("UTF8 description response") << QStringLiteral("data/reader_UTF8_description.xml") << true << 1 << 1 << QStringLiteral("123456789") << QStringLiteral("Sieger F - Zweiter E") << QStringLiteral("UTF8 characters: nœud") << false << 0; QTest::newRow("funny character in UID response") << QStringLiteral("data/reader_UID.xml") << true << 1 << 1 << QStringLiteral("123-456@789%369$258*147") << QStringLiteral("Sieger F - Zweiter E") << QStringLiteral("description") << false << 0; QTest::newRow("early UID, before VEVENT") << QStringLiteral("data/reader_earlyUID.xml") << true << 1 << 1 << QStringLiteral("1234567890abcdef") << QStringLiteral("early UID test") << QStringLiteral("") << false << 0; QTest::newRow("description with CR response") << QStringLiteral("data/reader_CR_description.xml") << true << 1 << 1 << QStringLiteral("123-456@789%369$258*147") << QStringLiteral("Sieger F - Zweiter E") << QStringLiteral("description\nmultilines\n") << false << 0; QTest::newRow("basic vcalendar response") << QStringLiteral("data/reader_basic_vcal.xml") << true << 1 << 1 << QStringLiteral("572a7c13-bbd6-4fce-9ebb-03a808111828") << QStringLiteral("Test") << QStringLiteral("Test description") << false << 0; QTest::newRow("missing version response") << QStringLiteral("data/reader_missing.xml") << true << 1 << 1 << QStringLiteral("123456789") << QStringLiteral("Sieger F - Zweiter E") << QStringLiteral("Achtelfinale") << false << 0; QTest::newRow("escaped xml tag within ics") << QStringLiteral("data/reader_xmltag.xml") << true << 1 << 1 << QStringLiteral("123456789") << QStringLiteral("Chrzęst gąsienic été") << QStringLiteral("&acef&g") << false << 0; QTest::newRow("xml tags and entities within cdata") << QStringLiteral("data/reader_cdata.xml") << true << 1 << 1 << QStringLiteral("123456789") << QStringLiteral("Regarder l'hôtel en Espagne") << QStringLiteral("&a<b>cef&g") << false << 0; QTest::newRow("url within ics description") << QStringLiteral("data/reader_urldescription.xml") << true << 1 << 1 << QStringLiteral("123456789") << QStringLiteral("Sieger F - Zweiter E") << QStringLiteral("this https://www.sailfishos.org/test?id=one&two=2 &\"' ok?") << false << 0; QTest::newRow("relative alarm time") << QStringLiteral("data/reader_relativealarm.xml") << true << 1 << 1 << QStringLiteral("123456789") << QStringLiteral("Alarm") << QStringLiteral("Alarm with relative time.") << false << 1; QTest::newRow("pending todo") << QStringLiteral("data/reader_todo_pending.xml") << true << 1 << 1 << QStringLiteral("20070313T123432Z-456553@example.com") << QStringLiteral("Submit Quebec Income Tax Return for 2006") << QString() << false << 0; } void tst_Reader::readICal() { QFETCH(QString, xmlFilename); QFETCH(bool, expectedNoError); QFETCH(int, expectedNResponses); QFETCH(int, expectedNIncidences); QFETCH(QString, expectedUID); QFETCH(QString, expectedSummary); QFETCH(QString, expectedDescription); QFETCH(bool, expectedRecurs); QFETCH(int, expectedNAlarms); QFile f(QStringLiteral("%1/%2").arg(QCoreApplication::applicationDirPath(), xmlFilename)); if (!f.exists() || !f.open(QIODevice::ReadOnly)) { QFAIL("Data file does not exist or cannot be opened for reading!"); } Reader rd; rd.read(f.readAll()); QCOMPARE(rd.hasError(), !expectedNoError); if (!expectedNoError) return; QCOMPARE(rd.results().size(), expectedNResponses); if (!rd.results().isEmpty()) QCOMPARE(rd.results().first().incidences.length(), expectedNIncidences); if (!expectedNIncidences) return; KCalendarCore::Incidence::Ptr ev = KCalendarCore::Incidence::Ptr(rd.results().first().incidences[0]); QCOMPARE(ev->uid(), expectedUID); QCOMPARE(ev->summary(), expectedSummary); QCOMPARE(ev->description(), expectedDescription); QCOMPARE(ev->recurs(), expectedRecurs); QCOMPARE(ev->alarms().length(), expectedNAlarms); } void tst_Reader::readDate_data() { QTest::addColumn("xmlFilename"); QTest::addColumn("expectedNoError"); QTest::addColumn("expectedNResponses"); QTest::addColumn("expectedNIncidences"); QTest::addColumn("expectedStartDate"); QTest::addColumn("expectedEndDate"); QTest::newRow("full day event") << QStringLiteral("data/reader_fullday.xml") << true << 1 << 1 << QDate(2017, 03, 24) << QDate(2017, 03, 24); QTest::newRow("vcalendar full days") << QStringLiteral("data/reader_fullday_vcal.xml") << true << 1 << 1 << QDate(2017, 03, 24) << QDate(2017, 03, 24); } void tst_Reader::readDate() { QFETCH(QString, xmlFilename); QFETCH(bool, expectedNoError); QFETCH(int, expectedNResponses); QFETCH(int, expectedNIncidences); QFETCH(QDate, expectedStartDate); QFETCH(QDate, expectedEndDate); QFile f(QStringLiteral("%1/%2").arg(QCoreApplication::applicationDirPath(), xmlFilename)); if (!f.exists() || !f.open(QIODevice::ReadOnly)) { QFAIL("Data file does not exist or cannot be opened for reading!"); } Reader rd; rd.read(f.readAll()); QCOMPARE(rd.hasError(), !expectedNoError); if (!expectedNoError) return; QCOMPARE(rd.results().size(), expectedNResponses); if (!rd.results().isEmpty()) QCOMPARE(rd.results().first().incidences.length(), expectedNIncidences); if (!expectedNIncidences) return; KCalendarCore::Event::Ptr ev = rd.results().first().incidences[0].staticCast(); QCOMPARE(ev->dtStart().date(), expectedStartDate); QCOMPARE(ev->dtEnd().date(), expectedEndDate); } void tst_Reader::readAlarm_data() { QTest::addColumn("xmlFilename"); QTest::addColumn("expectedNAlarms"); QTest::addColumn("expectedAction"); QTest::addColumn("expectedTime"); QTest::newRow("relative alarm time") << QStringLiteral("data/reader_relativealarm.xml") << 1 << int(KCalendarCore::Alarm::Display) << QStringLiteral("2017-03-23T12:00:00"); } void tst_Reader::readAlarm() { QFETCH(QString, xmlFilename); QFETCH(int, expectedNAlarms); QFETCH(int, expectedAction); QFETCH(QString, expectedTime); QFile f(QStringLiteral("%1/%2").arg(QCoreApplication::applicationDirPath(), xmlFilename)); if (!f.exists() || !f.open(QIODevice::ReadOnly)) { QFAIL("Data file does not exist or cannot be opened for reading!"); } Reader rd; rd.read(f.readAll()); QVERIFY(!rd.hasError()); QCOMPARE(rd.results().size(), 1); QCOMPARE(rd.results().first().incidences.length(), 1); KCalendarCore::Incidence::Ptr ev = KCalendarCore::Incidence::Ptr(rd.results().first().incidences[0]); QCOMPARE(ev->alarms().length(), expectedNAlarms); KCalendarCore::Alarm::Ptr alarm(ev->alarms().at(0)); QCOMPARE(alarm->type(), KCalendarCore::Alarm::Type(expectedAction)); QCOMPARE(alarm->time(), QDateTime::fromString(expectedTime, Qt::ISODate)); } #include "tst_reader.moc" QTEST_MAIN(tst_Reader) buteo-sync-plugin-caldav-0.3.14/tests/tests.pro000066400000000000000000000003131467717066200214560ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS += notebooksyncagent reader propfind caldavclient tests_xml.path = /opt/tests/buteo/plugins/caldav tests_xml.files = tests.xml INSTALLS += tests_xml OTHER_FILES += tests.xml buteo-sync-plugin-caldav-0.3.14/tests/tests.xml000066400000000000000000000017561467717066200214720ustar00rootroot00000000000000 /usr/sbin/run-blts-root /bin/su $USER -g privileged -c /opt/tests/buteo/plugins/caldav/tst_reader /usr/sbin/run-blts-root /bin/su $USER -g privileged -c /opt/tests/buteo/plugins/caldav/tst_propfind rm -f /tmp/testdb; SQLITESTORAGEDB=/tmp/testdb /usr/sbin/run-blts-root /bin/su $USER -g privileged -c /opt/tests/buteo/plugins/caldav/tst_notebooksyncagent /usr/sbin/run-blts-root /bin/su $USER -g privileged -c /opt/tests/buteo/plugins/caldav/tst_caldavclient